(Data, Variables and Calculations)

Wrox Press C++ Tutorial

Chapter 1 - Data, Variables and Calculations

The purpose of this tutorial is to give you a swift, basic grounding in C++. In a moment, we'll begin our journey with an introduction to the building blocks of the language. You'll learn how to get data from the keyboard into your programs, and how to get information onto the screen.

From these relatively humble beginnings, you'll progress through many of the other features of C++. They're presented one-by-one, with each new idea explained clearly, and demonstrated with a fully-worked example. By the end of the tutorial, you'll have the knowledge and the confidence to start experimenting for yourself.

As we explore aspects of the language using working examples, you'll have an opportunity to get some practice with the Microsoft Visual C++ Integrated Development Environment (IDE) - you should create a project for each of the examples before you build and execute them. Every project you create in this tutorial will be a Win32 Console Application, and you can find out exactly how to create one of those later on in this chapter.

In this chapter, we'll get down to the essentials of programming in C++. By the end of the chapter you will be able to write a simple C++ program of the traditional form: input - process - output.

In this chapter you will learn about:

  • C++ program structure

  • Namespaces

  • Variables in C++

  • Defining variables and constants

  • Basic input from the keyboard and output to the screen

  • Performing arithmetic calculations

  • Casting between types

  • Variable scope

© 1998 Wrox Press


(The Structure of a C++ Program)

The Structure of a C++ Program

Programs which will run as console applications under Visual C++ are text-based MS-DOS programs. All the examples that we'll write to understand how C++ works will be MS-DOS programs, so let's look at how such programs are structured.

A program in C++ consists of one or more functions. Every C++ program in the DOS environment contains the function main() and all C++ programs of any size consist of several functions. A function is simply a self-contained block of code with a unique name which is invoked by using the name of the function.

A typical DOS program might be structured as shown in the figure:

The figure above illustrates that execution of the program shown starts at the beginning of the function main(). From main(), execution transfers to a function input_names(), which returns execution to the position immediately following the point where it was called in main(). The function sort_names() is then called from main() and, once control returns to main(), the final function output_names() is called. Eventually, once output has been completed, execution returns once again to main() and the program ends.

Of course, different programs under DOS may have radically different functional structures, but they all start execution at the beginning of main(). The principal advantage of having a program broken up into functions is that you can write and test each piece separately. There is a further advantage in that functions written to perform a particular task can be re-used in other programs. The libraries that come with C++ provide a lot of standard functions that you can use in your programs. They can save you a great deal of work.

We'll see more about creating and using functions in Chapter 4.

Try It Out - A Simple Program

Let's look at a simple example to understand the elements of a program a little better. Start by creating a new project from the range of alternatives offered on the Projects tab when you click the New... item in the File menu. When the dialog appears, select Win 32 Console Application and name the project Ex1_01; then click on OK.

Next, the Console Application dialog appears. We'll start from the very basic project structure, so choose An empty project, and click on the Finish button.

The New Project Information tells you about the project you're about to create; it should look something like this:

The project directory at the foot of the screen may be slightly different on your machine; everything else should be identical. Once your happy with it, click on OK.

We'll start by entering the following program as a new source file, so click the leftmost icon on the standard toolbar, [image], to create a new text file into which you can type the code.

Note that this code is available for download from the Wrox Press website: http://www.wrox.com, as are all of the code examples in this tutorial.

// EX1_01.CPP
// A Simple Example of a Program
#include <iostream>
using namespace std;

int main()
{
   int apples, oranges;        // Declare two integer variables
   int fruit;                  // ...then another one

   apples = 5; oranges = 6;    // Set initial values
   fruit = apples + oranges;   // Get the total fruit

   cout << endl;               // Start output on a new line
   cout << "Oranges are not the only fruit... " << endl
        << "- and we have " << fruit << " fruits in all.";
   cout << endl;               // Start output on a new line

   return 0;                   // Exit the program
}

The above example is intended to illustrate some of the ways in which you can write C++ statements, rather than to be a model of good programming style.

Note that in the word endl ends with the letter l, not with the numeral 1.

Once you have keyed it in, save it as Ex1_01.cpp using the File menu Save As... option. The Save As dialog will offer to save it in the project directory that you just created and, since this file is part of that project, that's the best place for it. Since you have now identified the file by its extension as a file containing C++ code, the keywords in the code will gain their colors. You will be able to see if you have entered Int where you should have entered int, since the two will be different colors.

If you look at the FileView tab for your new project, you'll notice that the source file doesn't appear as part of the project's files. We need to add the file to the project using the Add to Project... item from the Project menu, or by right-clicking in the window containing the source and selecting Insert File into Project. Once this is done, you'll see the newly created source file in the FileView, and the main() function will appear under the Globals section of the ClassView. We'll consider the meaning of this later.

If you now build this program by using the Build button on the Project toolbar, [image], and execute it using the Execute Ex1_01.exe item in the Build menu, or by pressing the Execute Program button, [image], you should get the following output:

Program Comments

The first two lines in the C++ code are comments. Comments are an important part of any program, but they're not executable code - they are there simply to help the human reader. All comments are ignored by the compiler. On any line of code, two successive slashes // that are not contained within a text string (we shall see what text strings are later) indicate that the rest of the line is a comment.

You can see that several lines of the program contain comments as well as program statements. You can also use an alternative form of comment bounded by /* and */. For example, the first line of the program could have been written:

/*   EX1_01.CPP   */

The comment using // only covers the portion of the line following the two successive slashes, whereas the /*...*/ form defines whatever is enclosed as a comment and can span several lines. For example, we could write:

/*
   EX1_01.CPP
   A Simple Program Example
*/

All four lines are comments. If you want to highlight some particular comment lines, you can always embellish them with a frame of some description:

/*****************************
 *  EX1-01.CPP               *
 *  A Simple Program Example *
 *****************************/

As a rule, you should always comment your programs comprehensively. The comments should be sufficient for another programmer, or you at a later date, to understand the purpose of any particular piece of code, and to understand how it works.

The #include Directive - Header Files

Following the comments, we have the #include directive,

#include <iostream>

which makes the compiler insert the contents of the file iostream into the program before compilation. This file is called a header file because it's usually brought in at the beginning of a program file. This particular header file contains definitions that are necessary for you to be able to use input and output statements in C++. If we didn't include iostream in our program, it wouldn't compile because we use output statements which depend on some of the definitions in this file. There are many different header files provided by Visual C++ and they cover a wide range of capabilities. We shall be seeing more of them as we progress through the language facilities.

A #include statement is one of several preprocessor directives. The Visual C++ editor recognizes these and highlights them in blue in your edit window (as it does with all the C++ keywords). Preprocessor directives are commands executed by the compiler that generally act on your source code in some way before it is compiled. They all start with the # character. We'll be introducing other preprocessor directives as we need them.

The using Directive - Namespaces

The standard library is an extensive set of routines which have been written to do many common tasks: for example, dealing with input and output, performing basic mathematical calculations, etc. Instead of writing these routines for yourself, you can simply pluck them out of the standard library and use them in your code. The file iostream is just one of a number of header files that contain the routines of the standard library. (You can see a full list of the standard library files in the online MSDN help files in Visual C++ Documentation \ Reference \ C/C++ Language and C++ Libraries \ Standard C++ Library Reference.)

The code for all of these standard library routines is contained within the namespace std. So, any standard library routine belongs to the namespace std. Each of the standard library header files contributes a few routines to the namespace std.

The code in our program does not belong to the namespace std. Therefore, to use the output routines in the iostream header file, we need to tell the compiler that the routines belong to the namespace std. In order to do this, we use the using directive:

using namespace std;

With this line in our program, the compiler knows that we will be using routines that belong to the standard library. There are other ways of dealing with this, and we'll see those later, when we'll go into namespaces in more detail in Chapter 5, when we will be able to create our own namespaces.

The Function main()

The function main() in our example consists of the function header defining it as main() plus everything from the first opening curly brace, {, to the corresponding closing curly brace, },. The curly braces enclose the executable statements in the function, which are referred to collectively as the body of the function.

As we shall see, all functions consist of a header which defines (amongst other things) the function name, followed by the function body which consists of a number of program statements enclosed between a pair of curly braces. The body of a function may contain no statements at all, in which case it doesn't do anything.

A function that doesn't do anything may seem somewhat superfluous, but when you're writing a large program, you may map out the complete program structure in functions but, initially, leave the code for many of them with empty bodies. Doing this means that you can compile and execute the whole program with all its functions at any time, but add detailed coding for the functions incrementally.

Program Statements

The program statements making up the function body of main() are each terminated with a semicolon. The program statement is the basic unit in defining what a program does. This is a bit like a sentence in a paragraph of text, where each sentence stands by itself in expressing an action or an idea, but relates to and combines with the other sentences in the paragraph in expressing a more general idea. A statement is a self-contained definition of an action that the computer is to carry out, but which can be combined with other statements to define a more complex action or calculation.

The action of a function is always expressed by a number of statements, each ending with a semicolon. Let's take a quick look at each of the statements in the example that we have just written, just to get a general feel for how it works. We will discuss each type of statement more fully later in this chapter.

The first statement in the program,

int apples, oranges;        // Declare two integer variables

declares two variables, apples and oranges. A variable is a named bit of computer memory that you can use to store data. A statement introducing the names of variables is called a variable declaration. The keyword int indicates that the variables are to store values that are whole numbers, or integers. The next statement declares another integer variable, fruit. While you can declare several variables in the same statement, as we did for apples and oranges, it is generally a good idea to declare them separately. This enables you to comment them individually.

In the example, the line,

apples = 5; oranges = 6;    // Set initial values

contains two statements, each terminated by a semicolon. While it isn't obligatory, it's good programming practice to write only one statement on a line. The two statements store the values 5 and 6 in the variables apples and oranges respectively. These statements are called assignment statements, because they assign a new value to a variable.

The next statement,

fruit = apples + oranges;   // Get the total fruit

is also an assignment statement. This one adds the values stored in the variables apples and oranges and stores the result in the variable fruit.

The next three statements are:

cout << endl;               // Start output on a new line
cout << "Oranges are not the only fruit... " << endl
     << "- and we have " << fruit << " fruits in all.";
cout << endl;               // Start output on a new line

These are all output statements. The first sends a newline character, denoted by the word endl, to the screen. In C++, a source of input or a destination for output is referred to as a stream. The word cout specifies the 'standard' output stream, and the operator << indicates that what appears to the right of the operator is to be sent to the output stream, cout. The operator << 'points' in the direction that the data flows - from the variable or string on the right to the output destination on the left.

The meaning of the word cout and the operator << are defined by the contents of the header file iostream, which you'll remember we added to our program code by placing the #include directive at the beginning of the program. Because cout has been defined to send the standard output stream to your display screen, you shouldn't use the word cout for other purposes - for example, as a variable in your program.

The second statement sends a text string (defined between quotes) to the screen, followed by another newline character (endl), then another text string, followed by the value stored in the variable fruit, then finally another text string. There is no problem stringing together a sequence of things that you want to output in this way. The statement executes from left to right, with each item being sent to cout in turn. Note that each item is preceded by its own << operator.

The third statement sends another newline character to the screen. These statements produce the output from the program that you see. Note that the second statement runs over two lines. The successive lines are combined into a single statement until the compiler finds the semicolon that defines the end of the statement. This means that if you forget a semicolon for a statement, the compiler will assume the next line is part of the same statement and join them together. This usually results in something the compiler cannot understand, so you'll get an error when you try to compile the code.

The last statement in our program,

return 0;                   // Exit the program

stops execution of the program and returns control to the operating system. We will be discussing all of these statements in more detail later on.

The statements in a program are executed in the sequence in which they are written, unless a statement specifically causes the natural sequence to be altered. In Chapter 3, we will look at statements which alter the sequence of execution.

Whitespace

Whitespace is the term used in C++ to describe blanks, tabs, newline characters and comments. Whitespace separates one part of a statement from another and enables the compiler to identify where one element in a statement, such as int, ends and the next element begins. Therefore, in the statement,

int fruit;                  // ...then another one

there must be at least one whitespace character (usually a space) between int and fruit for the compiler to be able to distinguish them. On the other hand, in the statement

fruit = apples + oranges;   // Get the total fruit

no whitespace characters are necessary between fruit and =, or between = and apples, although you are free to include some if you wish. This is because the = is not alphabetic or numeric, so the compiler can separate it from its surroundings. Similarly, no whitespace characters are necessary either side of the + sign.

Apart from its use as a separator between elements in a statement that might otherwise be confused, whitespace is ignored by the compiler (except, of course, in a string of characters between quotes). You can, therefore, include as much whitespace as you like to make your program more readable, as we did when we spread our output statement in the last example over several lines. In some programming languages, the end of a statement is at the end of the line, but in C++ the end of a statement is wherever the semicolon occurs.

Since variable names must be made up of single words, you must not put whitespace characters in the middle. If you do, the single variable name won't be seen by the compiler as such, and it won't be interpreted correctly.

Statement Blocks

We can enclose several statements between a pair of curly braces, in which case they become a block, or a compound statement. The body of a function is an example of a block. Such a compound statement can be thought of as a single statement (as we shall see when we look at the decision making possibilities in C++ in the next chapter. In fact, wherever you can put a single statement in C++, you could equally well put a block of statements between braces. As a consequence, blocks can be placed inside other blocks. In fact, blocks can be nested, one within another, to any depth.

A statement block also has important effects on variables, but we will defer discussion of this until later in this chapter when we discuss something called variable scope.


(Defining Variables)

Defining Variables

Now that we are beyond our first program, we are going to want to manipulate some meaningful information and get some answers. An essential element in this process is having a piece of memory that we can call our own, that we can refer to using a meaningful name and where we can store an item of data. Each individual piece of memory so specified is called a variable.

Each variable will store a particular kind of data, which is fixed when we define the variable in our program. One variable might store whole numbers (that is, integers), in which case it couldn't be used to store numbers with fractional values. The value that each variable contains at any point is determined by the instructions in our program and, of course, its value will usually change many times as the program calculation progresses.

Let's look first at the rules for naming a variable when we introduce it into a program.

Naming Variables

The name we give to a variable is called an identifier, or more conveniently, a variable name. Variable names can include the letters A-z (upper or lower case), the digits 0-9 and the underscore character. All other characters are illegal. Variable names must also begin with either a letter or an underscore. Names are usually chosen to indicate the kind of information to be stored.

In Visual C++, variable names can be up to 247 characters long, which gives you a reasonable amount of flexibility. In fact, as well as variables, there are quite a few other things that have names in C++. We shall see they too can have names of up to 247 characters, with the same definition rules as a variable name. Using names of the maximum length can make your programs a little difficult to read and, unless you have amazing keyboard skills, they are the very devil to type in. A more serious consideration is that not all compilers support such long names. If you anticipate compiling your code in other environments, names with up to 31 characters are usually adequate and will not cause problems in most instances.

Although you can use variable names that begin with an underscore, for example _this and _that, this is best avoided, because there are potential clashes with standard system variables which have the same form. You should also avoid using names starting with a double underscore for the same reason.

Examples of good variable names are:

  • Price

  • discount

  • pShape

  • Value_

  • COUNT

8_Ball, 7Up, and 6_pack are not legal. Neither is Hash! or Mary-Ann. This last example is a common mistake, although Mary_Ann would be quite acceptable. Of course, Mary Ann would not be, because blanks are not allowed in variable names. Note that the variable names republican and Republican are quite different, as upper- and lower-case letters are differentiated.

Keywords in C++

There are reserved words in C++, also called keywords, which have special significance within the language. They will be highlighted with a particular color by the Visual C++ editor as you enter your program. If the keywords you type do not appear highlighted, then the keyword has been entered incorrectly.

Remember that keywords, like the rest of the C++ language, are case-sensitive.

For example, the program that you entered earlier in the chapter contained the keywords int and return. You will see many more as you progress through the book. You must ensure that the names you choose for entities in your program, such as variables, are not the same as any of the keywords in C++. You can find a complete list of the C++ keywords in the online help, if you look under Visual C++ Documentation \ Reference \ C/C++ Language and C++ Libraries \ C++ Language Reference \ Lexical Conventions \ C++ Keywords.

Declaring Variables

A variable declaration is a program statement which specifies the name of a variable and the sort of data that it can store. For example, the statement,

int value;

declares a variable with the name value that can store integers. The type of data that can be stored in the variable value is specified by the keyword int. Because int is a keyword, you can't use int as a name for one of your variables.

Note that a declaration always ends with a semicolon.

A single declaration can specify the names of several variables but, as we have said, it is generally better to declare variables in individual statements, one per line. We will deviate from this from time to time, but only in the interests of keeping the code reasonably compact.

A variable name alone can't store anything, so it's not much use on its own. In order to store data (for example, the value of an integer), we need to assign a piece of the computer's memory to the variable. This process is called variable definition.

In C++, a variable declaration is also a definition (except in a few special cases, which we shall come across during the book). In the course of a single statement, we introduce the variable name, and also tie it to an appropriately-sized piece of memory. So, the statement

int value;

is both a declaration and a definition. We use the variable name value that we declared, to access the piece of the computer's memory that we defined.

We use the term declaration when we introduce a name to our program, with information on what the name will be used for. The term definition refers to the allotment of computer memory to the name. In the case of variables, we can declare and define in a single statement, as in the line above.

You must declare a variable at some point between the beginning of your program and when the variable is used for the first time. In C++, it is good practice to declare variables close to their first point of use.

Initial Values for Variables

When you declare a variable, you can also assign an initial value to it. A variable declaration that assigns an initial value to a variable is called an initialization. To initialize a variable when you declare it, you just need to write an equals sign followed by the initializing value after the variable name. We can write the following statements to give each of the variables an initial value:

int value = 0;
int count = 10;
int number = 5;

In this case, value will have the value 0, count will have the value 10 and number will have the value 5. These three statements are each declarations, definitions and initializations.

There is another way of writing the initial value for a variable in C++ called functional notation. Instead of an equals sign and the value, you can simply write the value in parentheses following the variable name. So we could rewrite the previous declarations as:

int value(0);
int count(10);
int number(5);

If you don't supply an initial value for a variable, then it will usually contain whatever garbage was left in the memory location it occupies by the previous program you ran (there is an exception to this which we shall see later). Wherever possible, you should initialize your variables when you declare them. If your variables start out with known values, it makes it easier to work out what is happening when things go wrong. And if there's one thing you can be sure of, it's that things will go wrong.


(Data Types in C++)

Data Types in C++

The sort of information that a variable can hold is determined by its data type. All data and variables in your program must be of some defined type. C++ provides you with a range of standard data types, specified by particular keywords. We have already seen the keyword int for defining integer variables. As part of the object-oriented aspects of the language, you can also create your own data types, as we shall see later. For the moment, let's take a look at the elementary numerical data types that C++ provides.

Integer Variables

As we have said, integer variables are variables that can only have values that are whole numbers. The number of players in a football team is an integer, at least at the beginning of the game. We already know that you can declare integer variables using the keyword int. These are variables which occupy 4 bytes in memory and can take both positive and negative values.

The upper and lower limits for the values of a variable of type int correspond to the maximum and minimum signed binary numbers which can be represented by 32 bits (4 bytes). The upper limit for a variable of type int is 231-1, and the lower limit is -231.

In Visual C++, the keyword short also defines an integer variable, this time occupying two bytes. The keyword short is equivalent to short int.

C++ also provides another integer type, long, which can also be written as long int. In this case, we can write the statement,

long bigNumber = 1000000L, largeValue = 0L;

where we declare the variables bigNumber and largeValue with initial values 1000000 and 0 respectively. The letter L appended to the end of the values specifies that they are long integers. You can also use the small letter l for the same purpose, but it has the disadvantage that it is easily confused with the numeral 1.

We don't include commas when writing large numeric values in a program.

Integer variables declared as long occupy 4 bytes and since this is the same as variables declared as int using Visual C++ 6.0, they have the same range of values.

With other C++ compilers, long and long int may not be the same as int, so if you expect your programs to be compiled in other environments, don't assume that long and int are equivalent. For truly portable code, you should not even assume that an int is 4 bytes (for example, under older 16-bit versions of Visual C++ an int was 2 bytes).

The char Data Type

The char data type serves a dual purpose. It specifies a one-byte variable that you can use to store integers, or to store a single ASCII character, which is the American Standard Code for Information Interchange. We can declare a char variable with this statement:

char letter = 'A';

This declares the variable letter and initializes it with the constant 'A'. Note that we specify a value which is a single character between single quotes, rather than the double quotes which we used previously for defining a string of characters to be displayed. A string of characters is a series of values of type char, which are grouped together into a single entity called an array. We will discuss arrays and how strings are handled in C++ in Chapter 3.

Because the character 'A' is represented in ASCII by the decimal value 65, we could have written this:

char letter = 65;      // Equivalent to A

to produce the same result as the previous statement. The range of integers that can be stored in a variable of type char is from -128 to 127.

We can also use hexadecimal constants to initialize char variables (and other integer types). A hexadecimal number is written using the standard representation for hexadecimal digits: 0 to 9, and A to F (or a to f) for digits with values from 10 to 15. It's also preceded by 0x (or 0X) to distinguish it from a decimal value. Thus, to get exactly the same result again, we could rewrite the last statement as follows:

char letter = 0x41;    // Equivalent to A

Don't write decimal integer values with a leading zero. The compiler will interpret such values as octal (base 8), so a value written as 065 will be equivalent to 53 in normal decimal notation.

Integer Type Modifiers

Variables of the integral types char, int, short or long, which we have just discussed, contain signed values by default. That is, they can store both positive and negative values. This is because the default type modifier for these types is the modifier signed. So, wherever we wrote char, int, or long, we could have written signed char, signed int, or signed long respectively.

If you are sure that you don't need to store negative values in a variable (for example, if you were recording the number of miles you drive in a week), then you can specify a variable as unsigned:

unsigned long mileage = 0UL;

Here, the minimum value that can be stored in the variable mileage is zero, and the maximum value is 4,294,967,295 (that's 232-1). Compare this to the range of -2,147,483,648 to 2,147,483,647 for a signed long. The bit which is used in a signed variable to determine the sign, is used in an unsigned variable as part of the numeric value instead. Consequently, an unsigned variable has a larger range of positive values, but it can't take a negative value. Note how a U (or u) is appended to unsigned constants. In the above example, we also have appended L to indicate that the constant is long. You can use either upper or lower case for U and L and the sequence is unimportant, but it's a good idea to adopt a consistent way of specifying such values.

Of course, both signed and unsigned are keywords, so you can't use them as variable names.

Floating Point Variables

Values which aren't integral are stored as floating point numbers. A floating point number can be expressed as a decimal value such as 112.5, or with an exponent such as 1.125E2 where the decimal part is multiplied by the power of 10 specified after the E (for Exponent). Our example is, therefore, 1.125×102, which is 112.5.

A floating point constant must contain a decimal point, or an exponent, or both. If you write neither, you have an integer.

You can specify a floating point variable using the keyword double, as in this statement:

double in_to_mm = 25.4;

A double variable occupies 8 bytes of memory and stores values accurate to 15 decimal digits. The range of values stored is much wider than that indicated by the 15 digits accuracy, being from 1.7×10-308 to 1.7×10308, and can be either positive or negative. Note that you can't get exactly zero with a floating point number, which can occasionally cause some strange errors in calculations.

If you don't need 15 digits precision, and you don't need the massive range of values provided by double variables, you can opt to use the keyword float to declare floating point variables occupying 4 bytes. For example, the statement,

float pi = 3.14159f;

defines a variable pi with the initial value 3.14159. The f at the end of the constant specifies it to be a float type. Without the f, the constant would have been of type double. Variables declared as float are of 7 decimal digits precision and can have values from 3.4×10-38 to 3.4×1038, positive and negative.

You can find a complete summary of the various data types in the MSDN online documentation, provided with Visual C++ 6.0.

Logical Variables

Logical variables can only have two values: a value called true or a value called false. The type for a logical variable is bool, named after George Boole, who developed Boolean algebra. Variables of type bool are used to store the results of tests which can be either true or false, such as whether one value is equal to another. You can declare a variable of type bool with the statement:

bool testResult;

Of course, you can also initialize them when you declare them:

bool colorIsRed = true;

You will find that the values true and false are used quite extensively with variables of numeric type, and particularly of type int. This is a hangover from the time before variables of type bool were implemented in C++. The symbols TRUE and FALSE commonly represented the integers 1 and 0 respectively, which generally worked in the same way as the bool values true and false. Note that TRUE and FALSE are not keywords in C++, and they are not legal bool values.

Variables with Specific Sets of Values

You will sometimes be faced with the need for variables that have a limited set of possible values which can be usefully referred to by labels - the days of the week, for example, or months of the year. There is a specific facility in C++ to handle this situation, called an enumeration. Let's take one of the examples we have just mentioned - a variable that can assume values corresponding to days of the week. We can define this as follows:

enum Week {Mon, Tues, Wed, Thurs, Fri, Sat, Sun} this_week;

This declares an enumeration type called Week and the variable this_week, which is an instance of the enumeration type Week that can only assume the values specified between the braces. If you try to assign anything other than one of the set of values specified to this_week, it will cause an error. The symbolic names listed between the braces are known as enumerators. In fact, each of the names of the days will be automatically defined as representing a fixed integer value. The first name in the list, Mon, will have the value 0, Tues will be 1, and so on. By default, each successive enumerator is one larger than the value of the previous one. If you would prefer the implicit numbering to start at a value other than zero, you can just write

enum Week {Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun} this_week;

and they will be equivalent to 1 through 7. The enumerators don't even need to have unique values. You could define Mon and Tues as both having the value 1 for example, with the statement:

enum Week {Mon = 1, Tues = 1, Wed, Thurs, Fri, Sat, Sun} this_week;

As it's the same as an int, the variable this_week will occupy four bytes, as will all variables which are of an enumeration type.

Having defined the form of an enumeration, you can define another variable thus:

enum Week next_week;

This defines a variable next_week as an enumeration that can assume the values previously specified. You can also omit the keyword enum in declaring a variable so, instead of the previous statement, you could write:

Week next_week;

If you wish, you can assign specific values to all the enumerators. For example, we could define this enumeration:

enum Punctuation {Comma=',', Exclamation='!', Question='?'} things;

Here we have defined the possible values for the variable things as the numerical equivalents of the appropriate symbols. The ASCII codes for these symbols are 44, 33 and 63 respectively in decimal. As you can see, the values assigned don't have to be in ascending order. If you don't specify all the values explicitly, values continue to be assigned incrementing by 1 from the last specified value, as in our second Week example.

You can omit the enumeration type if you don't need to define other variables of this type later. For example:

enum {Mon, Tues, Wed, Thurs, Fri, Sat, Sun} thisWeek, nextWeek, lastWeek;

Here we have three variables declared that can assume values from Mon to Sun. Since the enumeration type is not specified, we cannot refer to it. Note that you cannot define other variables for this enumeration at all, since you would not be permitted to repeat the definition. Doing so would imply that you were redefining values for Mon to Sun, and this isn't allowed.

Defining Your Own Data Types

The typedef keyword enables you to define your own data type specifier. Using typedef, you could define the type name BigOnes as equivalent to the standard long int type with the declaration:

typedef long int BigOnes;       // Defining BigOnes as a type name

This defines BigOnes as an alternative type specifier for long int, so you could declare a variable mynum as long int with the declaration:

BigOnes mynum = 0L;              // Define a long int variable

There's no difference between this declaration and the one using the built-in type name. You could equally well use:

long int mynum = 0L;             // Define a long int variable

for exactly the same result. In fact, if you define your own type name such as BigOnes, you can use both type specifiers within the same program for declaring different variables that will end up as having the same type.

Since typedef only defines a synonym for an existing type, it may appear to be a bit superficial. We will see later that it can fulfill a very useful role in enabling us to simplify more complex declarations than we have met so far. We will also see later (in chapter 6) that classes provide us with a means of defining completely new data types.


(Basic Input/Output Operations)

Basic Input/Output Operations

C++ input/output revolves around the notion of a data stream, where we can insert data into an output stream or extract data from an input stream. We have already seen that the standard output stream to the screen is referred to as cout. The input stream from the keyboard is referred to as cin.

Input from the Keyboard

We obtain input from the keyboard through the stream cin, using the extractor operator for a stream, >>. To read two integer values from the keyboard into integer variables num1 and num2, you can write this:

cin >> num1 >> num2;

The operator 'points' in the direction that data flows - in this case, from cin to each of the two variables in turn. Any leading whitespace is skipped and the first integer value you key in is read into num1. This is because the input statement executes from left to right. Whitespace following num1 is ignored and the second integer value that you enter is read into num2. There has to be some whitespace between successive values though, so that they can be differentiated. The stream input operation ends when you press the Enter key and execution then continues with the next statement. Of course, errors can arise if you key in the wrong data, but we will assume that you always get it right!

Floating point values are read from the keyboard in exactly the same way as integers and, of course, we can mix the two. The stream input and operations automatically deal with variables and data of any of the basic types. For example, look at the statements,

int num1 = 0, num2 = 0;
double factor = 0.0;
cin >> num1 >> factor >> num2;

The last line will read an integer into num1, then a floating point value into factor and, finally, an integer into num2.

Try It Out - Output to the Display

Writing information to the display operates in a complementary fashion to input. The stream is called cout and we use the insertion operator, <<. This also 'points' in the direction of data movement. We have already used this operator to output a text string between quotes. We can demonstrate the process of outputting the value of a variable with a simple program. We'll assume that you've got the hang of creating a new project and a new source file, adding the source file to the project and building it into an executable. Here's the code:

// EX1_02.CPP
// Exercising output
#include <iostream>
using namespace std;

int main()
{
   int num1 = 1234, num2 = 5678;
   cout << endl;                                //Start on a new line
   cout << num1 << num2;                        //Output two values
   cout << endl;                                //End on a new line
   return 0;                                    //Exit program
}

How It Works

The first statement in the body of main() declares and initializes two integer variables, num1 and num2. This is followed by two output statements, the first of which moves the screen cursor position to a new line. Because output statements execute from left to right, the second output statement displays the value of num1 followed by the value of num2.

When you compile and execute this, you will get the output:

This is correct, but not exactly helpful. We really need the two output values to be separated by at least one space. The default for stream output is to just output the digits in the output value, which doesn't provide for spacing different values out nicely, so they can be differentiated. As it is, we have no way to tell where the first number ends and the second number begins.

Try It Out - Manipulators

We can fix this quite easily, though, just by outputting a space between the two values. We can do this by replacing the following line in our original program:

cout << num1 << num2;                        //Output two values

with the statement:

cout << num1 << ' ' << num2;                 //Output two values

Of course, if we had several rows of output that we wanted to align in columns, we would need some extra capability, as we do not know how many digits there will be in each value. We can take care of this situation by using what is called a manipulator. A manipulator modifies the way in which data output to (or input from) a stream is handled.

Manipulators are defined in the header file iomanip, so we need to add a #include statement for it. The manipulator that we will use is setw(n), which will output the following value right-justified in a field n spaces wide, so setw(6) puts the output in a field with a width of six spaces. To get something more like the output we want, we can change our program to the following:

// EX1_03.CPP
// Exercising output
#include <iostream>
#include <iomanip>
using namespace std;

int main()
{
   int num1 = 1234, num2 = 5678;
   cout << endl;                                //Start on a new line
   cout << setw(6) << num1 << setw(6) << num2;  //Output two values
   cout << endl;                                //Start on a new line
   return 0;                                    //Exit program
}

Note that the highlighted lines indicate the code that has changed compared to the previous example.

How It Works

The only changes from the last example are the addition of the #include statement for the file iomanip, and the insertion of the setw() manipulator in the output stream preceding each value, to output the values in a field six characters wide. Now we get nice neat output where we can actually separate the two values:

Note that the setw() manipulator only works for the single output value immediately following it. We have to insert it into the stream immediately preceding each value that we want to output within a given field width. If we put only one setw(), it would apply to the first value to be output after it was inserted. Any following value would be output in the default manner. You could try this out by deleting the second setw(6) and its insertion operator in the above example.

Escape Sequences

When we write a character string between quotes, we can include special characters called escape sequences. They are called escape sequences because they allow characters to be included in a string that otherwise could not be represented. An escape sequence starts with a backslash character, \. For example, a tab character is written as \t, so these two output statements,

cout << endl << "This is output.";
cout << endl << "\tThis is output after a tab.";

will produce these lines:

This is output.

This is output after a tab.

In fact, instead of using endl, we could include the escape sequence for the newline character, \n, in each string, so we could rewrite the statements above as follows:

cout << "\nThis is output.";
cout << "\n\tThis is output after a tab.";

Here are some escape sequences which may be particularly useful:

Escape sequence | What it does
\a | sounds a beep
\n | newline
\' | single quote
\\ | backslash
\b | backspace
\t | tab
\" | double quote

Obviously, if you want to be able to include a backslash or a double quote as a character to be output in a string between quotes, you must use the escape sequences to represent them. Otherwise, the backslash would be interpreted as another escape sequence and a double quote would indicate the end of the character string.

You can also use characters specified by escape sequences in the initialization of char variables. For example:

char Tab = '\t';      // Initialize with tab character

That gives us enough of a toehold in input/output. We will collect a few more bits and pieces as and when we need them.


(Calculating in C++)

Calculating in C++

This is where we actually start doing something with the data that we enter. We are beginning the 'processing' part of a C++ program. Almost all of the computational aspects of C++ are fairly intuitive, so we should slice through this like a hot knife through butter.

The Assignment Statement

We have already seen examples of the assignment statement. A typical assignment statement would look like this:

whole = part1 + part2 + part3;

The assignment statement enables you to calculate the value of an expression which appears on the right hand side of the equals sign (in this case the sum of part1, part2 and part3) and store the result in the variable specified on the left hand side (whole). In this statement, the whole is exactly the sum of its parts, and no more.

Note how the statement, as always, ends with a semicolon.

You can also write repeated assignments such as,

A = B = 1;

where this is equivalent to assigning the value 1 to B, then assigning the value of B to A.

Understanding Lvalues

The simplest definition of an lvalue is something that has an address in memory. Usually this is something that can appear on the left hand side of an equals sign in an assignment, although this is not necessarily the case. Variables are lvalues, since they specify a place in memory. However, there are lvalues that can't appear on the left of an assignment, because their values have been defined as constant. We'll be looking at such variables a bit later in this chapter. The variables A and B appearing in the preceding paragraph are lvalues, whereas the expression A+B would not be, since its result doesn't determine an address in memory where a value might be stored.

Arithmetic Operations

The basic arithmetic operators we have at our disposal are addition, subtraction, multiplication and division, represented by the symbols +, -, * and / respectively. These operate generally as you would expect, with the exception of division, which has a slight aberration when working with integer variables or constants, as we'll see. You can write statements like this:

netPay = hours * rate - deductions;

Here, the product of hours and rate will be calculated, then deductions subtracted from the value produced. The multiply and divide operators are executed before addition and subtraction. We will discuss the order of execution more fully later in this chapter. The overall result of the expression will be stored in the variable netPay.

The minus sign used in the last statement applies to two operands - it subtracts one from another. This is called a binary operation because two values are involved. The minus sign can also be used with one operand to change the sign of its value, in which case it is called a unary minus. You could write this:

int A = 0;
int B = -5;
A = -B;                        // Changes the sign of the operand

Here, A will be assigned the value +5, because the unary minus changes the sign of the value of the operand B.

Note that an assignment is not the equivalent of the equations you saw in high school algebra. It specifies an action to be carried out rather than a statement of fact. The statement,

A = A + 1;

means, 'add 1 to the current value stored in A and then store the result back in A'. As a normal algebraic statement it wouldn't make sense.

Try It Out - Exercising Basic Arithmetic

We can exercise basic arithmetic in C++ by calculating how many standard rolls of wallpaper are needed to paper a room. This is done with the following example:

// EX1_04.CPP
// Calculating how many rolls of wallpaper are required for a room
#include <iostream>
using namespace std;

int main()
{
   double height = 0.0, width = 0.0, length = 0.0; // Room dimensions
   double perimeter = 0.0;                         // Room perimeter

   const double rollwidth = 21.0;         // Standard roll width
   const double rolllength = 12.*33.;     // Standard roll length(33ft.)

   int strips_per_roll = 0;               // Number of strips in a roll
   int strips_reqd = 0;                   // Number of strips needed
   int nrolls = 0;                        // Total number of rolls

   cout << endl                           // Start a new line
        << "Enter the height of the room in inches: ";
   cin >> height;

   cout  << endl                          // Start a new line
         << "Now enter the length and width in inches: ";
   cin >> length >> width;

   strips_per_roll = rolllength/height; // Get number of strips in a roll
   perimeter = 2.0*(length + width);     // Calculate room perimeter
   strips_reqd = perimeter/rollwidth;    // Get total strips required
   nrolls = strips_reqd/strips_per_roll; // Calculate number of rolls

   cout << endl
        << "For your room you need " << nrolls << " rolls of wallpaper."
        << endl;

   return 0;
}

How It Works

One thing needs to be clear at the outset. No responsibility is assumed for you running out of wallpaper as a result of using this program! As we shall see, all errors in the estimate of the number of rolls required are due to the way C++ works and to the wastage that inevitably occurs when you hang your own wallpaper - usually 50%+!

We can work through the statements in this example in sequence, picking out the interesting, novel, or even exciting features. The statements down to the start of the body of main() are familiar territory by now, so we will take those for granted.

A couple of general points worth noting are about the layout of the program. First, the statements in the body of main() are indented to make the extent of the body easier to see and, second, various groups of statements are separated by a blank line to indicate that they are functional groups. The compiler just ignores such whitespace, but indenting statements and leaving some lines blank is a fundamental technique in laying out program code in C++. You will see that this is applied universally to provide visual cues for various logical blocks in a program.

The const Modifier

We have a block of declarations for the variables used in the program right at the beginning of the body of main(). These statements are also fairly familiar, but there are two which contain some new features:

const double rollwidth =21.0;          // Standard roll width
const double rolllength = 12.*33.;     // Standard roll length(33ft.)

They both start out with a new keyword: const. This is a type modifier which indicates that the variables are not just of type double, but are also constants. Because we effectively tell the compiler that these are constants, the compiler will check for any statements which attempt to change their values and, if it finds any, it will generate an error message. This is relatively easy since a variable declared as const cannot legally be placed on the left of an assignment operation.

You could check this out by adding, anywhere after the declaration of rollwidth, a statement such as:

rollwidth = 0;

You will find the program no longer compiles.

It can be very useful defining constants by means of const variable types, particularly when you use the same constant several times in a program. For one thing, if you need to change it, you will only need to change its definition at the beginning to ensure that the change automatically appears throughout. We'll see this technique used quite often.

Constant Expressions

The const variable rolllength is also initialized with an arithmetic expression (12.*33.). Being able to use constant expressions to initialize variables saves having to work out the value yourself, and can also be more meaningful, as 33 feet times 12 inches is much clearer than simply writing 396. The compiler will generally evaluate constant expressions accurately, whereas if you do it yourself, depending on the complexity of the expression and your ability to number-crunch, there is a finite probability that it may be wrong.

You can use any expression that can be calculated as a constant at compile time, including const objects that you have already defined. So, for instance, if it was useful in the program to do so, we could declare the area of a standard roll of wallpaper as:

const double rollarea = rollwidth*rolllength;

Obviously, this statement would need to be placed after the declarations for the two const variables used in the initialization of rollarea.

Program Input

The next four statements in the program handle input:

cout << endl                           // Start a new line
     << "Enter the height of the room in inches: ";
cin >> height;

cout << endl                          // Start a new line
     << "Now enter the length and width in inches: ";
cin >> length >> width;

Here we have used cout to prompt for the input required and then read the input from the keyboard using cin. We first obtain the room height and then read the length and width successively. In a practical program, we would need to check for errors, and possibly make sure that the values that are read are sensible, but we don't have enough knowledge to do that yet!

Calculating the Result

We have four statements involved in calculating the number of standard rolls of wallpaper required for the size of room given:

strips_per_roll = rolllength/height;   // Get number of strips in a roll
perimeter = 2.0*(length + width);      // Calculate room perimeter
strips_reqd = perimeter/rollwidth;     // Get total strips required
nrolls = strips_reqd/strips_per_roll;  // Calculate number of rolls

The first statement calculates the number of strips of paper with a length corresponding to the height of the room that we can get from a standard roll, by dividing one into the other. So, if the room is 8 feet high, we divide 96 into 396, which would produce the floating point result 4.125. There is a subtlety here, however. The variable where we store the result, strips_per_roll, was declared as int, so it can only store integer values. Consequently, any floating point value to be stored as an integer is rounded down to the nearest integer, 4 in our case, and this value is stored. This is actually the result that you want here since, although they may fit under a window or over a door, fractions of a strip are best ignored when estimating.

The conversion of a value from one type to another is called casting. This particular example is called an implicit cast, because the code doesn't explicitly state that a cast is needed, and the compiler has to work it out for itself. You should beware when using implicit casts. The compiler does not supply a warning that an implicit cast is being made, and if you are assigning a value of one type to a variable of a type with a lesser range of values, then there is a danger that you will lose information. If there are implicit casts in your program which you don't know about, then they will almost certainly form bugs which are difficult to locate.

Where such an assignment is unavoidable, you can specify the conversion explicitly to demonstrate that it is no accident and that you really meant to do it. We do this by making an explicit cast of the value on the right of the assignment to int, so the statement would become:

strips_per_roll = static_cast<int>(rolllength/height); //Get number of strips in a roll

The addition of static_cast<int>(), with the parentheses around the expression on the right, tells the compiler explicitly to convert the value of the expression to int. Although this means that we still lose the fractional part of the value, the compiler assumes that we know what we are doing and does not issue a warning. We'll see more about static_cast<>(), and other types of explicit casting, later in this chapter.

Note how we calculate the perimeter of the room in the next statement. In order to multiply the sum of the length and the width by two, we enclose the sum of the two variables between parentheses. This ensures that the addition is performed first and the result is then multiplied by 2.0 to give us the correct value for the perimeter. We can use parentheses to make sure that a calculation is carried out in the order we require, since expressions in parentheses are always evaluated first. Where there are nested parentheses, the expressions within the parentheses are evaluated in sequence, from the innermost to the outermost.

The third statement, calculating how many strips of paper are required to cover the room, uses the same effect that we observed in the first statement: the result is rounded down to the nearest integer, because it is to be stored in the integer variable, strips_reqd. This is not what we need in practice. It would be best to round up for estimating, but we don't have enough knowledge of C++ to do this yet. Once you have read the next chapter, you can come back and fix it!

The last arithmetic statement calculates the number of rolls required by dividing the number of strips required (integer) by the number of strips in a roll (also integer). Because we are dividing one integer by another, the result has to be integer, so any remainder is ignored. This would still be the case if the variable nrolls were floating point. The resulting integer value would be converted to floating point form before it was stored in nrolls. The result that we obtain is essentially the same as if we had produced a floating point result and rounded down to the nearest integer. Again, this is not what we want, so if you want to use this, you will need to fix it.

Displaying the Result

The result of the calculation is displayed by the following statement:

cout << endl
     << "For your room you need " << nrolls << " rolls of wallpaper."
     << endl;

This is a single output statement spread over three lines. It first outputs a newline character, then the text string "For your room you need ". This is followed by the value of the variable, nrolls, and finally the text string, " rolls of wallpaper.". As you can see, output statements are very easy in C++.

Finally, the program ends when this statement is executed:

return 0;

The value zero here is a return value which, in this case, will be returned to the operating system.

Calculating a Remainder

We have seen in the last example that dividing one integer value by another produces an integer result that ignores any remainder, so if you take 11 divided by 4, then you get the result 2. Since the remainder after division can be of great interest, particularly when you are dividing cookies amongst children, for example, C++ provides a special operator, %, for this. So we can write the statements,

int residue = 0, cookies = 19, children = 5;
residue = cookies % children;

and the variable residue will end up with the value 4 - the number left after dividing 19 by 5. To calculate how many each of them received, you just need to use division, as in the statement:

each = cookies / children;

This statements says that the result of the division cookies/children, 3, will be stored in the variable each.

Modifying a Variable

It's often necessary to modify the existing value of a variable, such as incrementing it or doubling it. We could increment a variable called count using the statement:

count = count + 5;

This simply adds 5 to the current value stored in count, and stores the result back in count, so if count started out at 10, it would end up as 15. You have an alternative, shorthand way of writing the same thing in C++:

count += 5;

This says, 'Take the value in count, add 5 to it and store the result back in count'. We can also use other operators with this notation. For example, the statement,

count *= 5;

has the effect of multiplying the current value of count by 5 and storing the result back in count. In general, we can write statements of the form,

lhs op= rhs;

where op is any of the following operators:

    +    -    *    /    %
    <<    >>    &    ^    |

The first five of these we have already met, and the remainder, which are shift and logical operators, we will see later in this chapter. lhs stands for any legal expression for the left-hand side of the statement, and is usually (but not necessarily) a variable name. rhs stands for any legal expression on the right-hand side of the statement.

The general form of the statement is equivalent to this:

lhs = lhs op (rhs);

This means that we can write statements such as,

A /= B + C;

which will be identical in effect to,

A = A/(B + C);

The Increment and Decrement Operators

We will now take a brief look at some unusual arithmetic operators called the increment and decrement operators, as we will find them to be quite an asset once we get further into applying C++ in earnest. These are unary operators, which are used to increment or decrement a variable. For example, assuming the variable count is of type int, the following three statements all have exactly the same effect:

count = count + 1;
count += 1;
++count;

They each increment the variable count by 1. The last form, using the increment operator, is clearly the most concise. If this action is contained within another expression then the action of the operator is to first increment the value of the variable, and then use the incremented value in the expression. For example, if count has the value 5, and the variable total is of type int, then the statement,

total = ++count + 6;

will result in count being incremented to 6, while total is then assigned the value 12.

So far, we have written the increment operator, ++, in front of the variable to which it applies. This is called the prefix form. The increment operator also has a postfix form, where the operator is written after the variable to which it applies; the effect of this is slightly different. The variable to which the operator applies is only incremented after its value has been used in context. For example, let's reset count to the value 5, and rewrite the previous example as:

total = count++ + 6;

Here, total is assigned the value 11, since the initial value of count is used to evaluate the expression before it is incremented by 1. The statement above is equivalent to the two statements:

total = count + 6;
++count;

The clustering of '+' signs, in the example of the postfix form above, is likely to lead to confusion. Generally, it isn't a good idea to write the increment operator in the way that we have here. It would be clearer to write:

total = 6 + count++;

Where we have an expression such as a++ + b, or even a+++b, it becomes less obvious what is meant or what the compiler will do. They are actually the same, but in the second case you might really have meant a + ++b, which is different - it evaluates to one more than the other two expressions.

Exactly the same rules that we have discussed in relation to the increment operator also apply to the decrement operator, --. For example, if count has the initial value 5, then the statement,

total = --count + 6;

results in total having the value 10 assigned, whereas,

total = 6 + count--;

sets the value of total to 11. Both operators are usually applied to integers, particularly in the context of loops, as we shall see in the next chapter. We shall also see in later chapters that they can be applied to other data types in C++.

Try It Out - The Comma Operator

The comma operator allows you to specify several expressions where normally only one might occur. This is best understood by looking at an example of how it is used:

// EX1_05.CPP
// Exercising the comma operator
#include <iostream>
using namespace std;

int main()
{
   long num1 = 0, num2 = 0, num3 = 0, num4 = 0;

   num4 = (num1 = 10, num2 = 20, num3 = 30);
   cout << endl
        << "The value of a series of expressions "
        << "is the value of the right most: "
        << num4;
   cout << endl;

   return 0;
}

How It Works

If you compile and run this program you will get this output:

This is fairly self-explanatory. The variable num4 receives the value of the last of the series of three assignments, the value of an assignment being the value assigned to the left-hand side. The parentheses in the assignment for num4 are essential. You could try executing this without them to see the effect. Without the parentheses, the first expression separated by commas in the series will become:

num4 = num1 = 10

So, num4 will have the value 10.

Of course, the expressions separated by the comma operator don't have to be assignments. We could equally well write the following:

long num1 = 1, num2 = 10, num3 = 100, num4 = 0;
num4 = (++num1, ++num2, ++num3);

The effect of this assignment would be to increment the variables num1, num2 and num3 by 1, and to set num4 to the value of the last expression which will be 101.

This example is aimed at illustrating the effect of the comma operator, and is not an example of how to write good code. As you can see, writing an assignment statement in this fashion can be very misleading.

The Sequence of Calculation

So far, we haven't talked about how we arrive at the sequence of calculations involved in evaluating an expression. It generally corresponds to what you will have learnt at school when dealing with basic arithmetic operators, but there are many other operators in C++. To understand what happens with these, we need to look at the mechanism used in C++ to determine this sequence. It's referred to as operator precedence.

Operator Precedence

Operator precedence orders the operators in a priority sequence. In any expression, operators with the highest precedence are always executed first, followed by operators with the next highest precedence, and so on, down to those with the lowest precedence of all. A list of all the C++ operators in order of their precedence can be found in the online help in Visual C++ Documentation \ Reference \ C/C++ Language and C++ Libraries \ C++ Language Reference \ Lexical Conventions \ C++ Operators.

If there are no parentheses in an expression, operators of equal precedence are executed in a sequence determined by their associativity. Thus, if the associativity is 'left to right', the left-most operator in an expression is executed first, progressing through the expression to the right-most.

Note that where an operator has a unary (working with one operand) and a binary (working with two operands) form, the unary form is always of a higher precedence and is, therefore, executed first.

You can always override the precedence of operators by using parentheses. Since there are so many operators in C++, it's sometimes hard to be sure what takes precedence over what. It is a good idea to insert parentheses to make sure. A further plus is that parentheses often make the code much easier to read.


(Variable Types and Casting)

Variable Types and Casting

Calculations in C++ can only be carried out between values of the same type. When you write an expression involving variables or constants of different types, for each operation to be performed the compiler has to convert the type of one of the operands to match that of the other. This conversion process is called casting. For example, if you want to add a double value to an integer, the integer value is first converted to double, after which the addition is carried out. Of course, the variable which contains the value to be cast is itself not changed. The compiler will store the converted value in a temporary memory location which will be discarded when the calculation is finished.

There are rules governing the selection of the operand to be converted in any operation. Any expression to be calculated can be broken down into a series of operations between two operands. For example, the expression 2*3-4+5 amounts to the series 2*3 resulting in 6, 6-4 resulting in 2, and finally 2+5 resulting in 7. Thus, the rules for casting operands where necessary only need to be defined in terms of decisions about pairs of operands. So, for any pair of operands of different types, the following rules are checked in the order that they are written. When one applies, that rule is used.

Rules for Casting Operands

  1. If either operand is of type long double, the other is converted to long double.

  2. If either operand is of type double, the other is converted to double.

  3. If either operand is of type float, the other is converted to float.

  4. Any operand of type char, signed char, unsigned char, short, or unsigned short is converted to type int.

  5. An enumeration type is converted to the first of int, unsigned int, long, or unsigned long that accommodates the range of the enumerators.

  6. If either operand is of type unsigned long, the other is converted to unsigned long.

  7. If one operand is of type long and the other is of type unsigned int, then both operands are converted to type unsigned long.

  8. If either operand is of type long, the other is converted to type long.

We could try these rules on a hypothetical expression to see how they work. Let's suppose that we have a sequence of variable declarations as follows:

double value = 31.0;
int count = 16;
float many = 2.0f;
char num = 4;

Let's also suppose that we have the following rather arbitrary arithmetic statement:

value = (value - count)*(count - num)/many + num/many;

We can now work out what casts the compiler will apply. The first operation is to calculate (value - count). Rule 1 doesn't apply but Rule 2 does, so the value of count is converted to double and the double result 15.0 is calculated. Next (count - num) must be evaluated, and here the first rule in sequence which applies is Rule 4, so num is converted from char to int, and the result 12 is produced as a value of type int. The next calculation is the product of the first two results, a double 15.0 and an int 12. Rule 2 applies here and the 12 is converted to 12.0 as double, and the double result 180.0 is produced. This result now has to be divided by many, so Rule 2 applies again and the value of many is converted to double before generating the double result 90.0. The expression num/many is calculated next, and here Rule 3 applies to produce the float value 2.0f after converting the type of num from char to float. Lastly, the double value 90.0 is added to the float value 2.0f for which Rule 2 applies, so after converting the 2.0f to 2.0 as double, the final result of 92.0 is stored in value as a double.

In spite of the last paragraph reading a bit like The Auctioneer's Song, I hope you get the general idea.

Casts in Assignment Statements

As we saw in example Ex1_04.cpp earlier in this chapter, you can cause an implicit cast by writing an expression on the right-hand side of an assignment that is of a different type to the variable on the left-hand side. This can cause values to be changed and information to be lost. For instance, if you assign a float or double value to an int or a long variable, the fractional part of the float or double will be lost and just the integer part will be stored. (You may lose even more information, if your floating point variable exceeds the range of values available for the integer type concerned.)

For example, after executing the following code fragment,

int number = 0;
float decimal = 2.5f;
number = decimal;

the value of number will be 2. Note the f at the end of the constant 2.5. This indicates to the compiler that this constant is single precision floating point. Without the f, the default would have been double. Any constant containing a decimal point is floating point. If you don't want it to be double precision, you need to append the f. A capital F would do the job just as well.

Explicit Casts

With mixed expressions involving the basic types, your compiler automatically arranges casting where necessary, but you can also force a conversion from one type to another by using an explicit cast. To cast the value of an expression to a given type, you write the cast in the form:

static_cast<the_type_to_convert_to>(expression)

The keyword static_cast reflects the fact that the cast is checked statically - that is, when your program is compiled. Later, when we get to deal with classes, we will meet dynamic casts, where the conversion is checked dynamically - that is, when the program is executing. The effect of the cast is to convert the value that results from evaluating expression to the type that you specify between the angled brackets. The expression can be anything from a single variable to a complex expression involving lots of nested parentheses.

Here's a specific example of the use of static_cast<>():

double value1 = 10.5;
double value2 = 15.5;
int whole_number = static_cast<int>(value1) + static_cast<int>(value2);

The initializing value for the variable whole_number is the sum of the integral parts of value1 and value2, so they are each explicitly cast to type int. The variable whole_number will therefore have the initial value 25. The casts do not affect the values stored in value1 and value2, which will remain as 10.5 and 15.5 respectively. The values 10 and 15 produced by the casts are just stored temporarily for use in the calculation and then discarded. Although both casts cause a loss of information in the calculation, the compiler will always assume that you know what you are doing when you explicitly specify a cast.

Also, as we described in Ex1_04.cpp, when relating to assignments with different types, you can always make it clear that you know the cast is necessary by making it explicit:

strips_per_roll = static_cast<int>(rolllength/height); //Get number of strips in a roll

You can write an explicit cast for any standard type, but you should be conscious of the possibility of losing information. If you cast a float or double value to long, for example, you will lose the fractional part of the value converted, so if the value started out as less than 1.0, the result will be 0. If you cast double to float, you will lose accuracy because a float variable has only 7 digits precision, whereas double variables maintain 15. Even casting between integer types provides the potential for losing data, depending on the values involved. For example, the value of an integer of type long can exceed the maximum that you can store in a variable of type short, so casting from a long value to a short may lose information.

In general, you should avoid casting as far as possible. If you find that you need a lot of casts in your program, the overall design of your program may well be at fault. You need to look at the structure of the program and the ways in which you have chosen data types to see whether you can eliminate, or at least reduce, the number of casts in your program.

C++ provides three other types of explicit cast, although these are not so common as the static_cast<>(). These types are:

  • reinterpret_cast<>() - this handles casts between unrelated types, for example, an integer to a pointer

  • const_cast<>() - this removes the const qualifier

  • dynamic_cast<>() - this allows you to cast between pointers (and references) to polymorphic classes in an inheritance hierarchy. Don't worry about what these terms mean at the moment, we'll discuss them in more detail in chapter 8.

Old-Style Casts

Prior to the introduction of the new style casts into C++, an explicit cast of the result of an expression to another type was written as:

(the_type_to_convert_to)expression

The result of expression is cast to the type between the parentheses. For example, the statement to calculate strips_per_roll in our previous example could be written:

strips_per_roll = (int)(rolllength/height); //Get number of strips in a roll

Essentially, there are four different kinds of casts, and the old-style casting syntax covers them all. Because of this, code using the old-style casts is more error prone - it is not always clear what you intended, and you may not get the result you expected. Although you will still see the old style of casting used extensively (it's still part of the language), I strongly recommend that you stick to using only the new casts in your code.

The Bitwise Operators

The bitwise operators treat their operands as a series of individual bits rather than a numerical value. They only work with either integer variables or constants as operands, so only data types short, int, long and char can be used. They are useful in programming hardware devices, where the status of a device is often represented as a series of individual flags (that is, each bit of a byte may signify the status of a different aspect of the device), or for any situation where you might want to pack a set of on-off flags into a single variable. You will see them in action when we look at input/output in detail, where single bits are used to control various options in the way data is handled.

There are six bitwise operators:

& Bitwise AND | Bitwise OR ^ Bitwise Exclusive OR

~ Bitwise NOT >> Shift right << Shift left

Let's take a look at how each of them works.

The Bitwise AND

The bitwise AND, &, is a binary operator that combines corresponding bits in its operands. If both corresponding bits are 1, the result is a 1 bit, and if either or both operand bits are 0, the result is a 0 bit.

The effect of a particular binary operator is often shown using what is called a truth table. This shows, for various possible combinations of operands, what the result is. The truth table for & is as follows:

Bitwise AND | 0 | 1
0 | 0 | 0
1 | 0 | 1

For each row and column combination, the result of & combining the two is the entry at the intersection of the row and column. Let's see how this works in an example:

char Letter1 = 'A', Letter2 = 'Z', Result = 0;
Result = Letter1 & Letter2;

We need to look at the bit patterns to see what happens. The letters 'A' and 'Z' correspond to hexadecimal values 0x41 and 0x5A respectively (see Appendix B for ASCII codes). The way in which the bitwise AND operates on these two values is shown below:

You can confirm this by looking at how corresponding bits combine with & in the truth table. After the assignment, Result will have the value 0x40, which corresponds to the character '@'.

Because the & produces zero if either bit is zero, we can use this operator to make sure that unwanted bits are zero in a variable. We achieve this by creating what is called a 'mask' and combining with the original variable using &. We create the mask by putting 1 where we want to keep a bit, and 0 where we want to set a bit to zero. The result will be 0s where the mask bit is 0, and the same value as the original bit in the variable where the mask is 1. Suppose we have a char variable, Letter, where, for the purposes of illustration, we want to eliminate the high order 4 bits, but keep the low order 4 bits. (Remember that a char variable occupies one byte, which is eight bits.) This is easily done by setting up a mask as 0x0F and combining it with the letter using & like this,

Letter = Letter & 0x0F;

or, more concisely:

Letter &= 0x0F;

If Letter started out as 0x41, it would end up as 0x01 as a result of either of these statements. This operation is clearly illustrated in the diagram below:

The 0 bits in the mask cause corresponding bits in Letter to be set to 0, and the 1 bits in the mask cause corresponding bits to be kept.

Similarly, you can use a mask of 0xF0 to keep the 4 high order bits, and zero the 4 low order bits. Therefore, this statement,

Letter &= 0xF0;

will result in the value of Letter being changed from 0x41 to 0x40.

The Bitwise OR

The bitwise OR, |, sometimes called the inclusive OR, combines corresponding bits such that the result is a 1 if either operand bit is a 1, and 0 if both operand bits are 0. The truth table for the bitwise OR is:

Bitwise OR | 0 | 1
0 | 0 | 1
1 | 1 | 1

We can exercise this with an example of how we could set individual flags packed into a variable of type int. Let's suppose that we have a variable called style, of type short, which contains 16 individual 1-bit flags. Let's suppose further that we are interested in setting individual flags in the variable style. One way of doing this is by defining values that we can combine with the OR operator to set particular bits on. To use in setting the rightmost bit, we can define:

short VREDRAW=0x01;

For use in setting the second-to-rightmost bit, we could define the variable hredraw as:

short HREDRAW=0x02;

So we could set the rightmost two bits in the variable style to 1 with the statement:

style = HREDRAW|VREDRAW;

The effect of this statement is illustrated in the diagram below:

Because the OR operation results in 1 if either of two bits is a 1, ORing the two variables together produces a result with both bits set on.

A very common requirement is to be able to set flags in a variable without altering any of the others which may have been set elsewhere. We can do this quite easily with a statement such as:

style |= HREDRAW|VREDRAW;

This statement will set the two rightmost bits of the variable style to 1, leaving the others at whatever they were before the execution of this statement.

The Bitwise Exclusive OR

The exclusive OR, ^, is so called because it operates similarly to the inclusive OR but produces 0 when both operand bits are 1. Therefore, its truth table is as follows:

Bitwise EOR | 0 | 1
0 | 0 | 1
1 | 1 | 0

Using the same variable values that we used with the AND, we can look at the result of the following statement:

result = letter1^letter2;

This operation can be represented as:

letter1     0100 0001
letter2     0101 1010

EORed together produce:

result      0001 1011

The variable result is set to 0x1B, or 27 in decimal notation.

The ^ operator has a rather surprising property. Suppose that we have two char variables, first with the value 'A', and last with the value 'Z', corresponding to binary values 0100 0001 and 0101 1010. If we write the statements,

first ^= last;           // Result first is 0001 1011
last ^= first;           // Result last is 0100 0001
first ^= last;           // Result first is 0101 1010

the result of these is that first and last have exchanged values without using any intermediate memory location. This works with any integer values.

The Bitwise NOT

The bitwise NOT, ~, takes a single operand for which it inverts the bits: 1 becomes 0, and 0 becomes 1. Thus, if we execute the statement,

result = ~letter1;

and letter1 is 0100 0001, then the variable result will have the value 1011 1110, which is 0xBE, or 190 as a decimal value.

The Bitwise Shift Operators

These operators shift the value of an integer variable a specified number of bits to the left or right. The operator >> is for shifts to the right, while << is the operator for shifts to the left. Bits that 'fall off' either end of the variable are lost. The illustration below shows the effect of shifting the 2 byte variable left and right, with the initial value shown.

This one's on the production drive as 59_02_05.cdr

We declare and initialize a variable called number with the statement:

unsigned short number = 16387U;

As we saw in the last chapter, we should write unsigned literals with a letter U or u appended to the number. We can shift the contents of this variable with the statement:

number <<= 2;         // Shift left two bit positions

The left operand of the shift operator is the value to be shifted, and the number of bit positions that the value is to be shifted is specified by the right operand. The illustration shows the effect of the operation. As you can see, shifting the value 16,387 two positions to the left produces the value 12. The rather drastic change in the value is the result of losing the high order bit.

We can also shift the value to the right. Let's reset the value of number to its initial value of 16,387. Then we can write:

number >>= 2;         // Shift right two bit positions

This shifts the value 16,387 two positions to the right, storing the value 4,096 in the variable number. Shifting right two bits is effectively dividing the value by 4 (without remainder). This is also shown in the illustration.

As long as bits are not lost, shifting n bits to the left is equivalent to multiplying the value by 2, n times. In other words, it is equivalent to multiplying by 2n. Similarly, shifting right n bits is equivalent to dividing by 2n. But beware: as we saw with the left shift of the variable number, if significant bits are lost, the result is nothing like what you would expect. However, this is no different from the multiply operation. If you multiplied the two-byte number by four you would get the same result, so shifting left and multiply are still equivalent. The problem of accuracy arises because the value of the result of the multiply is outside the range of a two-byte integer.

You might imagine that confusion could arise with the operators that we have been using for input and output. As far as the compiler is concerned, the meaning will always be clear from the context. If it isn't, the compiler will generate a message, but you need to be careful. For example, if you want to output the result of shifting a variable number left by two bits, you could write:

cout << (number << 2);

Here, the parentheses are essential. Without them, the shift operator will be interpreted by the compiler as a stream operator, so you won't get the result that you intended.

In the main, the right shift operation is similar to the left shift. For example, if the variable number has the value 24, and we execute the statement,

number >>= 2;

it will result in number having the value 6, effectively dividing by 4. However, the right shift operates in a special way with signed integer types that are negative (that is, the sign bit, which is the leftmost bit, is 1). In this case, the sign bit is propagated to the right. For example, let's declare and initialize a variable number, of type char, with the value -104 in decimal:

char number = -104;  // Binary representation is 1001 1000

Now we can shift it right 2 bits with the operation:

number >>= 2;        // Result 1110 0110

The decimal value of the result is -26, as the sign bit is repeated. With operations on unsigned integer types, of course, the sign bit is not repeated and zeros appear.

These shift operations can be faster than the regular multiply or divide operations on some computers - on an Intel 80486, for example, a multiply is slower than a shift left by at least a factor of 3. However, you should only use them in this way if you are sure you are not going to lose bits that you can ill afford to be without.


(Understanding Scope)

Understanding Scope

All variables have a finite lifetime when your program executes. They come into existence from the point at which you declare them and then, at some point, they disappear - at the latest, when your program terminates. How long a particular variable lasts is determined by a property called its storage duration. There are three different kinds of storage duration that a variable can have:

  • automatic storage duration

  • static storage duration

  • dynamic storage duration

Which of these a variable will have depends on how you create it. We will defer discussion of variables with dynamic storage duration until Chapter 3, but we can look into the characteristics of the other two in this chapter.

Another property that variables have is scope. The scope of a variable is simply that part of your program in which the variable name is valid. Within a variable's scope, you can legally refer to it, to set its value or use it in an expression. Outside of the scope of a variable, you cannot refer to its name - any attempt to do so will cause a compiler error. Note that a variable may still exist outside of its scope, even though you cannot refer to it by name. We will see examples of this situation a little later in this discussion.

All of the variables that we have declared up to now have had automatic storage duration, and are therefore called automatic variables. Let's take a closer look at these first.

Automatic Variables

The variables that we have declared so far have been declared within a block - that is, within the extent of a pair of curly braces. These are called automatic variables and are said to have local scope or block scope. An automatic variable is 'in scope' from the point at which it is declared until the end of the block containing its declaration.

An automatic variable is 'born' when it is declared and automatically ceases to exist at the end of the block containing the declaration. This will be at the closing brace matching the first opening brace that precedes the declaration of the variable. Every time the block of statements containing a declaration for an automatic variable is executed, the variable is created anew, and if you specified an initial value for the automatic variable, it will be reinitialized each time it is created.

There is a keyword, auto, which you can use to specify automatic variables, but it is rarely used since it is implied by default. Let's put together an example of what we've discussed so far.

Try It Out - Automatic Variables

We can demonstrate the effect of scope on automatic variables with the following example:

// EX1_06.CPP
// Demonstrating variable scope
#include <iostream>
using namespace std;

int main()
{                                         // Function scope starts here
   int count1 = 10;
   int count3 = 50;
   cout << endl
        << "Value of outer count1 = " << count1
        << endl;

   {                                // New scope starts here...
      int count1 = 20;        // This hides the outer count1
      int count2 = 30;
      cout << "Value of inner count1 = " << count1
           << endl;
      count1 += 3;            // This affects the inner count1
      count3 += count2;
   }                                // ...and ends here

   cout << "Value of outer count1 = " << count1
        << endl
        << "Value of outer count3 = " << count3
        << endl;

   // cout << count2 << endl; // uncomment to get an error

   return 0;
}                                         // Function scope ends here

How It Works

The output from this example will be:

The first two statements declare and define two integer variables, count1 and count3, with initial values of 10 and 50 respectively. Both these variables exist from this point to the closing brace at the end of the program. The scope of these variables also extends to the closing brace at the end of main().

Remember that the lifetime and scope of a variable are two different things. It's important not to get these two ideas confused.

Following the variable definitions, the value of count1 is output to produce the first of the lines shown above.

There is then a second curly brace which starts a new block. Two variables, count1 and count2, are defined within this block, with values 20 and 30 respectively. The count1 declared here is different from the first count1. The first count1 still exists, but its name is masked by the second count1. Any use of the name count1 following the declaration within the inner block refers to the count1 declared within that block.

The variable name count1 has been duplicated here only to illustrate what happens. Although this code is legal, it isn't a good approach to programming in general. It's confusing, and it's very easy to hide variables defined in an outer scope accidentally.

The value shown in the second output line shows that within the inner block, we are using the count1 in the inner scope - that is, inside the innermost braces:

cout << "Value of inner count1 = " << count1
     << endl;

Had we still been using the outer count1, then this would display the value 10. The variable count1 is then incremented by the statement:

count1 += 3;            // This affects the inner count1

The increment applies to the variable in the inner scope, since the outer one is still hidden. However, count3, which was defined in the outer scope, is incremented in the next statement without any problem:

count3 += count2;

This shows that the variables which were declared at the beginning of the outer scope are accessible from within the inner scope. (Note that if count3 had been declared after the second of the inner pair of braces, then it would still be within the outer scope, but in that case count3 would not exist when the above statement is executed.)

After the brace ending the inner scope, count2 and the inner count1 cease to exist. The variables count1 and count3 are still there in the outer scope and the values displayed show that count3 was indeed incremented in the inner scope.

If you uncomment the line,

// cout << count2 << endl; // uncomment to get an error

then the program will no longer compile correctly, because it attempts to output a non-existent variable. You will get an error message something like:

C:\Program Files\MyProjects\Ex1_06\Ex1_06.cpp(29): error C2065: 'count2': undeclared identifier

This is because count2 is out of scope at this point.

Positioning Variable Declarations

You have great flexibility in where you place the declarations for your variables. The most important aspect to consider is what scope the variables need to have. Beyond that, you should generally place a declaration close to where the variable is to be first used in a program. You should write your programs with a view to making them as easy as possible for another programmer to understand, and declaring a variable at its first point of use can be helpful in achieving that.

It is possible to place declarations for variables outside of all of the functions that make up a program. Let's look what effect that has on the variables concerned.

Global Variables

Variables that are declared outside of all blocks and classes (we will discuss classes later in the book) are called globals and have global scope (which is also called global namespace scope or file scope). This means that they are accessible throughout all the functions in the file, following the point at which they are declared. If you declare them at the very top of your program, they will be accessible from anywhere in the file.

Globals also have static storage duration by default. Global variables with static storage duration will exist from the start of execution of the program, until execution of the program ends. If you do not specify an initial value for a global variable, it will be initialized with 0 by default. Initialization of global variables takes place before the execution of main() begins, so they are always ready to be used within any code that is within the variable's scope.

The illustration below shows the contents of a source file, Example.cpp, and the arrows indicate the scope of each of the variables.

The variable value1 appearing at the beginning of the file is declared at global scope, as is value4, which appears after the function main(). The scope of each global variable extends from the point at which it is defined to the end of the file. Even though value4 exists when execution starts, it cannot be referred to in main() because main() is not within the variable's scope. For main() to use value4, you would need to move its declaration to the beginning of the file. Both value1 and value4 will be initialized with 0 by default, which is not the case for the automatic variables. Note that the local variable called value1 in function() hides the global variable of the same name.

Since global variables continue to exist for as long as the program is running, you might ask the question, 'Why not make all variables global and avoid this messing about with local variables that disappear?' This sounds very attractive at first, but as with the Sirens of mythology, there are serious side effects which completely outweigh any advantages you may gain.

Real programs are generally composed of a large number of statements, a significant number of functions and a great many variables. Declaring all variables at the global scope greatly magnifies the possibility of accidental erroneous modification of a variable, as well as making the job of naming them sensibly quite intractable. They will also occupy memory for the duration of program execution. By keeping variables local to a function or a block, you can be sure they have almost complete protection from external effects, they will only exist and occupy memory from the point at which they are defined to the end of the enclosing block, and the whole development process becomes much easier to manage.

If you take a look at ClassView for any of the examples that you have created so far, and extend the class tree for the project by clicking on the +, you will see an entry called Globals. If you extend this, you will see a list of everything in your program that has global scope. This will include all the global functions, as well as any global variables that you have declared.

Try It Out - The Scope Resolution Operator

As we have seen, a global variable can be hidden by a local variable with the same name. However, it's still possible to get at the global variable using the scope resolution operator, ::. We can demonstrate how this works with a revised version of the last example:

// EX1_07.CPP
// Demonstrating variable scope
#include <iostream>
using namespace std;

int count1 = 100;                         // Global version of count1

int main()
{                                         // Function scope starts here
   int count1 = 10;
   int count3 = 50;
   cout << endl
        << "Value of outer count1 = " << count1
        << endl;
   cout << "Value of global count1 = " << ::count1   // From outer block
        << endl;

   {                                // New scope starts here...
      int count1 = 20;              //This hides the outer count1
      int count2 = 30;
      cout << "Value of inner count1 = " << count1
           << endl;
      cout << "Value of global count1 = " << ::count1 // From inner block
           << endl;

      count1 += 3;                  // This affects the inner count1
      count3 += count2;
   }                                // ...and ends here.

   cout << "Value of outer count1 = " << count1
        << endl
        << "Value of outer count3 = " << count3
        << endl;

   //cout << count2 << endl;  // uncomment to get an error

   return 0;
}                                         // Function scope ends here

How It Works

If you compile and run this example, you'll get the following output:

The shaded lines indicate the changes we have made to the previous example; we just need to discuss the effects of those. The declaration of count1 prior to the definition of the function main() is global, so in principle it is available anywhere through the function main(). This global variable is initialized with the value of 100:

int count1 = 100;                         // Global version of count1

However, we have two other variables called count1, which are defined within main(), so throughout the program the global count1 is hidden by the local count1 variables. The first new output statement is:

cout << "Value of global count1 = " << ::count1              // From outer block
     << endl;

This uses the scope resolution operator (::) to make it clear to the compiler that we want to reference the global variable count1, not the local one. You can see that this works from the value displayed in the output.

In the inner block, the global count1 is hidden behind two variables called count1: the inner count1 and the outer count1. We can see the global scope resolution operator doing its stuff within the inner block, as you can see from the output generated by the statement we have added there:

cout << "Value of global count1 = " << ::count1              // From inner block
     << endl;

This outputs the value 100, as before - the long arm of the scope resolution operator used in this fashion always reaches a global variable.

We mentioned namespaces earlier in this chapter, when discussing the namespace std - we accessed the namespace std by employing the using directive. Alternatively, we can access a namespace by using the scope resolution operator - for example, we can write std::endl to access the end-of-line operator in the standard library. In the example above, we are using the scope resolution operator to search the global namespace for the variable count1. By not specifying a namespace in front of the operator, the compiler knows that must it search the global namespace for the name that follows it.

We'll be seeing a lot more of this operator when we get to talking about object-oriented programming, in which context it is used extensively. We'll also talk further about namespaces, including how to create your own, in Chapter 5.

Static Variables

It's conceivable that you might want to have a variable that's defined and accessible locally, but which also continues to exist after exiting the block in which it is declared. In other words, you need to declare a variable within a block scope, but to give it static storage duration. The static specifier provides you with the means of doing this, and the need for this will become more apparent when we come to deal with functions in Chapter 4.

In fact, a static variable will continue to exist for the life of a program even though it is declared within a block and only available from within that block (or its sub-blocks). It still has block scope, but it has static storage duration. To declare a static integer variable called count, you would write:

static int count;

If you don't provide an initial value for a static variable when you declare it, then it will be initialized for you. The variable count declared here will be initialized with 0. The default initial value for a static variable is always 0, converted to the type applicable to the variable. Remember that this is not the case with automatic variables. If you don't initialize your automatic variables, they will contain junk values left over from the program that last used the memory they occupy.


(Summary)

Summary

In this chapter, we have covered the basics of computation in C++. We have learnt about all of the elementary types of data provided for in the language, and all the operators that manipulate these types directly. The essentials of what we have discussed up to now are as follows:

  • A DOS program in C++ consists of at least one function called main().

  • The executable part of a function is made up of statements contained between curly braces.

  • A statement in C++ is terminated by a semicolon.

  • Named objects in C++, such as variables or functions, can have names that consist of a sequence of letters and digits, the first of which is a letter, and where an underscore is considered to be a letter. Upper and lower case letters are distinguished.

  • All the objects, such as variables, that you name in your program must not have a name that coincides with any of the reserved words in C++.

  • All constants and variables in C++ are of a given type. The basic types are char, int, long, float, and double.

  • The name and type of a variable is defined in a declaration statement ending with a semicolon. Variables may also be given initial values in a declaration.

  • You can protect the value of a variable of a basic type by using the modifier const. This will prevent direct modification of the variable within the program and give you compiler errors everywhere that a constant's value is altered.

  • By default, a variable is automatic, which means that it only exists from the point at which it is declared to the end of the scope in which it is defined, indicated by the corresponding closing brace after its declaration.

  • A variable may be declared as static, in which case it continues to exist for the life of the program. It can only be accessed within the scope in which it was defined.

  • Variables can be declared outside of all blocks within a program, in which case they have global namespace scope. Variables with global namespace scope are accessible throughout a program, except where a local variable exists with the same name as the global variable. Even then, they can still be reached by using the scope resolution operator.

  • The Standard Library contains functions and operators which you can use in your program. They are contained in the namespace std. This namespace is usually accessed with the using directive; individual objects in the namespace can be accessed by using the scope resolution operator.

  • An lvalue is an object that can appear on the left-hand side of an assignment. Non-const variables are examples of lvalues.

  • You can mix different types of variables and constants in an expression, but they will be automatically converted to a common type where necessary. Conversion of the type of the right hand side of an assignment to that of the left-hand side will also be made where necessary. This can cause loss of information when the left-hand side type can't contain the same information as the right-hand side: double converted to int, or long converted to short, for example.

  • The keyword typedef allows you to define synonyms for other types.

Although we have discussed all the basic types, don't be misled into thinking that's all there is. There are more complex types based on the basic set as we shall see, and eventually you will be creating original types of your own.


(Decisions and Loops)

Chapter 2 - Decisions and Loops

In this chapter, we will look at how to add decision-making capabilities to your C++ programs. You will also learn how to make your programs repeat a set of actions until a specific condition is met. This will enable you to handle variable amounts of input, as well as make validity checks on the data that you read in. You will also be able to write programs that can adapt their actions depending on the input data, and to deal with problems where logic is fundamental to the solution. By the end of this chapter you will have learnt:

  • How to compare data values

  • How to alter the sequence of program execution based on the result

  • What logical operators and expressions are and how you can apply them

  • How to deal with multiple choice situations

  • How to write and use loops in your programs

We will start with one of the most powerful programming tools: the ability to compare variables and expressions with other variables and expressions and, based on the outcome, execute one set of statements or another.


(Comparing Values)

Comparing Values

Unless we want to make decisions on a whim for the rest of our lives, we need a mechanism for comparing things. This involves some new operators called relational operators. Because all information in your computer is ultimately represented by numerical values (we saw in the last chapter how character information is represented by numeric codes), comparing numerical values is the essence of practically all decision making. We have six fundamental operators for comparing two values:

<
 less than
<=
 less than or equal to
>
 greater than
>=
 greater than or equal to
==
 equal to
!=
 not equal to

The 'equal to' comparison operator has two successive '=' signs. This is not the same as the assignment operator, which only consists of a single '=' sign. It's a pretty common mistake to use the assignment operator instead of the comparison operator, so watch out for this potential cause of confusion.

Each of these operators compares two values returns one of the two possible Boolean values: true if the comparison is true, or false if it is not. We can see how this works by having a look at a few simple examples of comparisons. Suppose we have integer variables i and j with the values 10 and -5 respectively. Then the expressions,

i > j          i!= j          j > 8          i <= j+15

all return the value true.

Let's further assume that we have the following variables defined:

char first = 'A', last = 'Z';

We can now write some examples of comparisons using character variables. Take a look at these:

first == 65;    first < last    'E' <= first    first != last

All four of these involve comparing ASCII code values. The first expression returns true, since first was initialized with 'A', which is the equivalent of decimal 65. The second expression checks whether the value of first, which is 'A', is less than the value of last, which is 'Z'. The capital letters are represented by an ascending sequence of numerical values from 65 to 90, 65 representing 'A' and 90 representing 'Z', so this comparison will also return the value true. The third expression returns the value false, since 'E' is greater than the value of first. The last expression returns true, since 'A' is definitely not equal to 'Z'.

Let's consider some slightly more complicated numerical comparisons. We can define some variables with the following statements:

int i = -10, j = 20;
double x = 1.5, y = -0.25E-10;

Take a look at these expressions:

-1 < y          j lt; (10-i)          2.0*x >= (3+y)

As you can see, we can use expressions resulting in a numerical value as operands in comparisons. Note that none of the parentheses are strictly necessary, because addition, subtraction, multiplication and division have higher precedence that the relational operators. They do, however, help to make the expressions clearer. The first comparison is true and so returns the bool value true. The variable y has a very small negative value, -0.000000000025, and so is greater than -1. The second comparison returns the value false - the expression 10-i has the value 20 which is the same as j. The third expression returns true, since the expression 3+y is slightly less than 3.

We can use relational operators to compare values of any of the basic types, so all we need now is a practical way of using the results of a comparison to modify the behavior of a program. Let's look into that right now.

The if Statement

The basic if statement allows your program to execute a single statement, or a block of statements enclosed within curly braces, if a given condition returns the value true. This is illustrated in this figure:

A simple example of an if statement is:

if(letter == 'A')
   cout << "The first capital, alphabetically speaking.";

The condition to be tested appears in parentheses immediately following the keyword, if. Note the position of the semicolon here. It goes after the statement following the if; there shouldn't be a semicolon after the condition in parentheses. You can also see how the statement following the if is indented to indicate that it is only executed when the if condition returns the value true. The indentation is not necessary for the program to execute, but it helps you to recognize the relationship between the if condition and the statement that depends on it.

The output statement will only be executed if the variable letter has the value 'A'. We could extend this example to change the value of letter if it contains the value 'A':

if(letter == 'A')
{
   cout << "The first capital, alphabetically speaking.";
   letter = 'a';
}

Here we execute the statements in the block only if the condition (letter == 'A') returns the value true. Without the curly braces, only the first statement would be the subject of the if, and the statement assigning the value 'a' to letter would always be executed. Note that there is a semicolon after each of the statements in the block, and not after the closing brace at the end of the block. There can be as many statements as you like within the block. Now, as a result of letter having the value 'A', we change its value to 'a' after outputting the same message as before. If the condition returns false, then neither of these statements will be executed.

Nested if Statements

The statement that is to be executed when the condition in an if statement is true can also be an if. This arrangement is called a nested if. The condition for the inner if is only tested if the condition for the outer if is true. An if that is nested inside another can also contain a nested if. You can generally continue nesting ifs one inside the other like this for as long as you know what you are doing.

Try It Out - Using Nested Ifs

We can demonstrate the nested if with a working example:

// EX2_01.CPP
// A nested if demonstration
#include <iostream>
using namespace std;

int main()
{
   char letter = 0;            // Store input in here

   cout << endl
        << "Enter a letter: "; // Prompt for the input
   cin >> letter;              // then read a character

   if(letter >= 'A')           // Test for 'A' or larger
      if(letter <= 'Z')        // Test for 'Z' or smaller
      {
         cout << endl
              << "You entered a capital letter."
              << endl;
         return 0;
      }

   if(letter >= 'a')           // Test for 'a' or larger
      if(letter <= 'z')        // Test for 'z' or smaller
      {
         cout << endl
              << "You entered a small letter."
              << endl;
         return 0;
      }

   cout << endl << "You did not enter a letter." << endl;
   return 0;
}

How It Works

This program starts with the usual comment lines, then the #include statement for the header file supporting input/output, and the using directive because those input/output routines belong to the namespace std. The first action in the body of main() is to prompt for a letter to be entered. This is stored in the char variable letter.

The if statement that follows the input checks whether the character entered is 'A' or larger. Since the ASCII codes for lower case letters (97 to 122) are greater than those for upper case letters (65 to 90), entering a lower case letter causes the program to execute the first if block, as (letter >= 'A') will return true for all letters. In this case, the nested if, which checks for an input of 'Z' or less is executed. If it is 'Z' or less, then we know that we have a capital letter, so the message is displayed and we are done. We execute a return statement to end the program. Both statements are enclosed between braces, so they are both executed when the nested if condition returns true.

The next if checks whether the character entered is lower case, using essentially the same mechanism as the first if, displays a message and returns.

If the character entered is not a letter, then the output statement following the last if block will be executed. This displays a message to the effect that the character entered was not a letter. The return is then executed.

You can see that the relationship between the nested ifs and the output statement is much easier to follow because of the indentation applied to each.

A typical output from this example is:

You could easily arrange to change upper case to lower case by adding just one extra statement to the if, checking for upper case:

   if(letter >= 'A')           // Test for 'A' or larger
      if(letter <= 'Z')        // Test for 'Z' or smaller
      {
         cout << endl
              << "You entered a capital letter.";
              << endl;
         letter += 'a' - 'A';  // Convert to lower case
         return 0;
      }

This involves adding one additional statement. This statement for converting from upper to lower case increments the letter variable by the value 'a' - 'A'. It works because the ASCII codes for 'A' to 'Z' and 'a' to 'z' are two groups of consecutive numerical codes, so the expression 'a' - 'A' represents the value to be added to an upper case letter to get the equivalent lower case letter.

You could equally well use the equivalent ASCII values for the letters here, but by using the letters we've ensured that this code would work on computers where the characters were not ASCII, as long as both the upper and lower case sets are represented by a contiguous sequence of numeric values.

There is a library function provided with Visual C++ to convert letters to upper case, so you don't normally need to program for this yourself. It has the name toupper() and appears in the standard library file cctype. You will see more about standard library facilities when we get to look specifically at how functions are written.

The Extended if Statement

The if statement that we have been using so far executes a statement if the condition specified returns true. Program execution then continues with the next statement in sequence. We also have a version of the if, which allows one statement to be executed if the condition returns true, and a different statement to be executed if the condition returns false. Execution then continues with the next statement in sequence. Remember that a block of statements can always replace a single statement, so this also applies to these ifs.

Try It Out - Extending the If

Here's an extended if example:

// EX2_02.CPP
// Using the extended if
#include <iostream>
using namespace std;

int main()
{
   long number = 0;  // Store input here
   cout << endl
        << "Enter an integer number
                  less than 2 billion: ";
   cin >> number;

   if(number%2L)     // Test remainder after division by 2
      cout << endl   // Here if remainder 1
           << "Your number is odd." << endl;
   else
      cout << endl   // Here if remainder 0
           << "Your number is even." << endl;

   return 0;
}

Typical output from this program is:

How It Works

After reading the input value into number, the value is tested by taking the remainder after division by two (using the remainder operator, %, that we saw in the last chapter) and using that as the condition for the if. In this case, the condition of the if statement returns an integer, not a Boolean. The if statement interprets a non-zero value returned by the condition as true, and interprets zero as false. In other words, the statement,

if(number%2L)

is equivalent to:

if(number%2L != 0)

If the remainder is 1, then the condition is true, and statement immediately following the if is executed. If the remainder is 0, then the condition is false, and the statement following the else keyword is executed.

The relational operators return only the values true or false. In an if statement, the condition may also take the form of any of the basic data types that we saw in the last chapter. In such a case, the if statement always interprets a non-zero value returned by the condition as true, and a zero as false.

Since the remainder of a division of an integer by two can only be one or zero, we have commented the code to indicate this fact. After either outcome, the return statement is executed to end the program.

The else keyword is written without a semicolon, similar to the if part of the statement. Again, indentation is used as a visible indicator of the relationship between various statements. You can clearly see which statement is executed for a true or non-zero result, and which for a false or zero result. You should always indent the statements in your programs to show their logical structure.

The if-else combination provides a choice between two options. The general logic of the if-else is shown here:

The arrows in the diagram indicate the sequence in which statements are executed, depending on whether the if condition returns true or false.

Nested if-else Statements

As we have seen, you can nest if statements within if statements. You can also nest if-else statements within ifs, ifs within if-else statements, and if-else statements within if-else statements. This provides us with considerable room for confusion, so let's look at a few examples. Taking the first case first, an example of an if-else nested within an if might be:

if(coffee == 'y')
   if(donuts == 'y')
      cout << "We have coffee and donuts.";
   else
      cout << "We have coffee, but not donuts";

The test for donuts is executed only if the result of the test for coffee returns true, so the messages reflect the correct situation in each case. However, it is easy to get this confused. If we write much the same thing with incorrect indentation, we can be trapped into the wrong conclusion:

if(coffee == 'y')
   if(donuts == 'y')
      cout << "We have coffee and donuts.";
else            // This else is indented incorrectly
   cout << "We have no coffee...";    // Wrong!

This mistake is easy to see here, but with more complicated if structures we need to keep in mind the rule about which if owns which else.

An else always belongs to the nearest preceding if which is not already spoken for by another else.

Whenever things look a bit complicated you can apply this rule to sort things out. When you are writing your own programs you can always use braces to make the situation clearer. It isn't really necessary in such a simple case, but we could write the last example as follows:

if(coffee == 'y')
{
   if(donuts == 'y')
      cout << "We have coffee and donuts.";
   else
      cout << "We have coffee, but not donuts";
}

This should make it should be absolutely clear. Now that we know the rules, understanding the case of an if nested within an if-else becomes easy:

if(coffee == 'y')
{
   if(donuts == 'y')
      cout << "We have coffee and donuts.";
}
else
   if(tea == 'y')
      cout << "We have tea, but not coffee";

Here the braces are essential. If we leave them out, the else would belong to the if which is looking out for donuts. In this kind of situation, it is easy to forget to include them, and hence create an error that may be hard to find. A program with this kind of error will compile fine, and even produce the right results some of the time.

If we removed the braces in this example, we'd get the right results only as long as coffee and donuts are both equal to 'y' so that the if(tea == 'y') check wouldn't be executed.

Here we'll look at if-else statements nested in if-else statements. This can get very messy, even with just one level of nesting. Let's beat the coffee and donuts analysis to death by using it again:

if(coffee == 'y')
   if(donuts == 'y')
      cout << "We have coffee and donuts.";
   else
      cout << "We have coffee, but not donuts";
else
   if(tea == 'y')
      cout << "We have no coffee, but we have tea,
                                 and maybe donuts...";
   else
      cout << "No tea or coffee, but maybe donuts...";

The logic here doesn't look quite so obvious, even with the correct indentation. No braces are necessary, as the rule you saw earlier will verify that this is correct, but it would look a bit clearer if we included them:

if(coffee == 'y')
{
   if(donuts == 'y')
      cout << "We have coffee and donuts.";
   else
      cout << "We have coffee, but not donuts";
}
else
{
   if(tea == 'y')
      cout << "We have no coffee, but we have tea,
                                 and maybe donuts...";
   else
      cout << "No tea or coffee, but maybe donuts...";
}

There are much better ways of dealing with this kind of logic in a program. If you put enough nested ifs together, you can almost guarantee a mistake somewhere. The following section will help to simplify things.

Logical Operators and Expressions

As we have just seen, using ifs where we have two or more related conditions can be a bit cumbersome. We have tried our iffy talents on looking for coffee and donuts, but in practice you may want to check much more complex conditions. You could be searching a personnel file for someone who is over 21 but under 35, female with a college degree, unmarried and speaks Hindi or Urdu. Defining a test for this could involve the mother of all ifs.

Logical operators provide a neat and simple solution. Using logical operators, we can combine a series of comparisons into a single expression, so we end up needing just one if, virtually regardless of the complexity of the set of conditions.

We have just three logical operators:

&&
 logical AND
||
 logical OR
!
 logical negation (NOT)

We'll first consider what each of these is used for in general terms, then we'll look at an example.

Logical AND

You would use the AND operator, &&, where you have two conditions that must both be true for a true result. You want to be rich and healthy. For example, you could use the && operator when you are testing a character to determine whether it's an upper case letter. The value being tested must be both greater than or equal to 'A' AND less than or equal to 'Z'. Both conditions must return true for the value to be a capital letter.

As before, these conditions may return numerical values. Remember that a non-zero value is treated as true, while zero is treated as false. Conversely, the bool value true has the value 1 when converted to an integer, while false has the value 0.

Taking the example of a value stored in a char variable letter, we could replace the test using two ifs for one that uses only a single if and the && operator:

if((letter >='A') && (letter<='Z'))
   cout << "This is a capital letter.";

The parentheses inside the if expression ensure that there is no doubt that the comparison operations are executed first, which makes the statement clearer. Here, the output statement will be executed only if both of the conditions combined by the operator && are true.

Just as with binary operators in the last chapter, we can represent the effect of a particular logical operator using a truth table. The truth table for && is as follows:

&& | 0 | 1
0 | 0 | 0
1 | 0 | 1

The row headings of the left and the column headings at the top represent the value of the logical expressions to be combined by the operator &&. Thus, to determine the result of combining a true condition (1) with a false condition (0), select the row with 1 at the left and the column with 0 at the top, and look at the intersection of the row and column for the result (0).

Logical OR

The OR operator, ||, applies when you have two conditions and you want a true result if either one of them is true or both are true. For example, you might be considered creditworthy for a loan from the bank if your income was at least $100,000 a year, or you had $1,000,000 in cash. This could be tested using the following if:

if((income >= 100000.00) || (capital >= 1000000.00))
   cout << "How much would you like to borrow,
              Sir, (grovel, grovel)?";

The ingratiating response emerges when either or both of the conditions are true. (A better response might be, "Why do you want to borrow?" It's strange how banks will only lend you money if you don't need it.)

We can also construct a truth table for the || operator:

||
 0 1
0 | 0 | 1
1 | 1 | 1

As you can see, you only get a false result if both conditions are false.

Logical NOT

The third logical operator, !, takes one operand with a logical value, true or false, and inverts its value. So if the value of test is true then !test is false, and if test is false then !test is true. To take the example of a simple expression, if x has the value 10, the expression:

!(x>5)

is false, since x>5 is true.

We could also apply the ! operator in an expression that was a favorite of Charles Dickens:

!(income>expenditure)

If this expression is true, the result is misery, at least as soon as the bank starts bouncing your checks.

Finally, we can apply the ! operator to other basic data types. Suppose we have a variable rate of type float, which has the value 3.2. For some reason we might want to use the expression:

!(rate)

The value 3.2 is non-zero, and converts to the Boolean true: so the result of this expression is false.

Try It Out - Combining Logical Operators

You can combine conditional expressions and logical operators to any degree that you feel comfortable with. For example, we could construct a test for whether a variable contained a letter just using a single if. Let's write it as a working example:

// EX2_03.CPP
// Testing for a letter using logical operators
#include <iostream>
using namespace std;

int main()
{
   char letter = 0;             // Store input in here

   cout << endl
        << "Enter a character: ";
   cin >> letter;

   // Test for alphabetic
   if(((letter>='A')&&(letter<='Z')) ||
      ((letter>='a')&&(letter<='z')))
      cout << endl
           << "You entered a letter." << endl;
   else
      cout << endl
           << "You didn't enter a letter." << endl;

   return 0;
}

How It Works

This example starts out in the same way as Ex3_01.cpp, by reading a character after a prompt for input. The interesting part of the program is in the if condition. This consists of two logical expressions combined with the || (OR) operator, so that, if either is true, the condition returns true and the message:

You entered a letter.

is displayed. If both logical expressions are false, then the else statement is executed which displays the message:

You didn't enter a letter.

Each of the logical expressions combines a pair of comparisons with the operator && (AND), so both comparisons must return true if the logical expression is to be true. The first logical expression returns true if the input is an upper case letter, and the second returns true if the input is a lower case letter.

The Conditional Operator

The conditional operator is sometimes called the ternary operator because it involves three operands. It is best understood by looking at an example. Suppose we have two variables, a and b, and we want to assign the maximum of a and b to a third variable c. We can do this with the statement:

c = a>b ? a : b;       // Set c to the maximum of a and b

The first argument of the conditional operator is a logical expression, in this case a>b. If this expression returns true then the second operand-in this case a-is selected as the value resulting from the operation. If the first argument returns false then the third operand-in this case b-is selected as the value. Thus, the result of the conditional expression a>b ? a : b is a if a is greater than b, and b otherwise. This value is stored in c. The use of the conditional operator in this assignment statement is equivalent to the if statement:

if(a>b)
   c = a;
else
   c = b;

The conditional operator can be written generally as:

condition ? expression1 : expression2

If the condition evaluates as true, then the result is the value of expression1, and if it evaluates to false, then the result is the value of expression2.

Try It Out - Using the Conditional Operator with Output

A common use of the conditional operator is to control output, depending on the result of an expression or the value of a variable. You can vary a message by selecting one text string or another depending on the condition specified.

// EX2_04.CPP
// The conditional operator selecting output
#include <iostream>
using namespace std;

int main()
{
   int nCakes = 1;           // Count of number of cakes

   cout << endl
        << "We have " << nCakes << " cake"
        << ((nCakes>1) ? "s." : ".")
        << endl;

   ++nCakes;

   cout << endl
        << "We have " << nCakes << " cake"
        << ((nCakes>1) ? "s." : ".")
        << endl;

   return 0;
}

The output from this program will be:

How It Works

First we initialize the nCakes variable with the value 1, then perform an output statement that shows us the number of cakes. The part that uses the conditional operator simply tests the variable to determine whether we have a singular cake or plural cakes:

((nCakes>1) ? "s." : ".")

This expression evaluates to "s." if nCakes is greater than 1, or "." otherwise. This allows us to use the same output statement for any number of cakes. We show this in the example by incrementing the nCakes variable and repeating the output statement.

There are many other situations where you can apply this sort of mechanism; selecting between "is" and "are", for example.

The switch Statement

The switch statement enables you to select from multiple choices based on a set of fixed values for a given expression. It operates like a physical rotary switch in that you can select one of a fixed number of choices; some makes of washing machine provide a means of choosing an operation for processing your laundry in this way. There are a given number of possible positions for the switch, such as cotton, wool, synthetic fiber, and so on, and you can select any one of them by turning the knob to point to the option that you want.

In the switch statement, the selection is determined by the value of an expression that you specify. You define the possible switch positions by one or more case values, a particular one being selected if the value of the switch expression is the same as the particular case value. There is one case value for each possible choice in the switch - the case values must be distinct.

If the value of the switch expression does not match any of the case values, then the switch automatically selects the default case. You can, if you wish, specify the code for the default case, as we will do below; otherwise, the default is to do nothing.

Try It Out - The Switch Statement

We can examine how the switch statement works with the following example:

// EX2_05.CPP
// Using the switch statement
#include <iostream>
using namespace std;

int main()
{
   int choice = 0;       // Store selection value here

   cout << endl
        << "Your electronic recipe book
                           is at your service." << endl
        << "You can choose from the following delicious
                                      dishes: " << endl
        << endl << "1 Boiled eggs"
        << endl << "2 Fried eggs"
        << endl << "3 Scrambled eggs"
        << endl << "4 Coddled eggs" << endl
        << endl << "Enter your selection number: ";
   cin >> choice;

   switch(choice)
   {
      case 1: cout << endl << "Boil some eggs."
                   << endl;
              break;
      case 2: cout << endl << "Fry some eggs."
                   << endl;
              break;
      case 3: cout << endl << "Scramble some eggs."
                   << endl;
              break;
      case 4: cout << endl << "Coddle some eggs."
                   << endl;
              break;
      default: cout << endl <<"You entered a wrong number,
                                       try raw eggs."
                    << endl;
   }

   return 0;
}

How It Works

After defining your options in the stream output statement, and reading a selection number into the variable choice, the switch statement is executed with the condition specified as simply choice in parentheses, immediately following the keyword switch. The possible options in the switch are enclosed between braces and are each identified by a case label. A case label is the keyword case, followed by the value of choice that corresponds to this option, and terminated by a colon.

As you can see, the statements to be executed for a particular case are written following the colon at the end of the case label, and are terminated by a break statement. The break transfers execution to the statement after the switch. The break isn't mandatory, but if you don't include it, all the statements for the cases following the one selected will be executed, which isn't usually what you want. You can demonstrate this by removing the break statements from this example and seeing what happens.

If the value of choice doesn't correspond with any of the case values specified, the statements preceded by the default label are executed. A default case isn't essential. In its absence, if the value of the test expression doesn't correspond to any of the cases, the switch is exited and the program continues with the next statement after the switch.

Try It Out - Sharing a Case

Each of the case constant expressions must be constant and unique. The reason that no two case constants can be the same is that the compiler would have no way of knowing which case statement should be executed for that particular value. However, different cases don't need to have a unique action. Several cases can share the same action, as shown here:

// EX2_06.CPP
// Multiple case actions
#include <iostream>
using namespace std;

int main()
{
   char letter = 0;
   cout << endl
        << "Enter a small letter: ";
   cin >> letter;

   switch(letter*(letter>='a' && letter <='z'))
   {
      case 'a':
      case 'e':
      case 'i':
      case 'o':
      case 'u': cout << endl << "You entered a vowel.";
                break;
      case 0: cout << endl << "It is not a small letter.";
              break;
      default: cout << endl << "You entered a consonant.";
   }

   cout << endl;
   return 0;
}

How It Works

In this example, we have a more complex expression in the switch. If the character entered isn't a lower case letter, then the expression:

(letter>='a' && letter<='z')

will result in the value 0. Then, letter is multiplied by this expression, so the switch expression would be set to 0 if a lower case letter wasn't entered. This will then cause the statements following the case label case 0 to be executed.

If a lower case letter was entered, the expression above will result in the value 1. Multiplying letter by 1 results in the switch expression having the same value as letter. For all values corresponding to vowels, the same output statement is executed since we haven't used break statements to separate these case labels. You can see that a single action can be taken for a number of different cases by writing each of the case labels one after the other before the statements to be executed. If a lower case letter that is a consonant is entered as program input, the default case label statement is executed.

Unconditional Branching

The if statement provides you with the flexibility to choose to execute one set of statements or another, depending on a specified condition, so the statement execution sequence is varied depending on the values of the data in the program. The goto statement, in contrast, is a blunt instrument. It enables you to branch to a specified program statement unconditionally. The statement to be branched to must be identified by a statement label, which is an identifier defined according to the same rules as a variable name. This is followed by a colon and placed before the statement requiring labeling. Here is an example of a labeled statement:

myLabel: cout << "myLabel branch has been activated"
              << endl;

This statement has the label myLabel. An unconditional branch to this statement would be written as follows:

goto myLabel;

Whenever possible, you should avoid using gotos in your program. They tend to encourage very convoluted code that can be extremely difficult to follow.

As the goto is theoretically unnecessary - there's always an alternative approach to using goto - a significant cadre of programmers say you should never use it. I don't subscribe to such an extreme view. It is a legal statement after all, and there are occasions when it can be convenient. However, I do recommend that you only use it where you can see an obvious advantage over other options that are available.


(Repeating a Block of Statements)

Repeating a Block of Statements

The ability to repeat a group of statements is fundamental to most applications. Without this ability, an organization would need to modify the payroll program every time an extra employee was hired, and you would need to reload Tetris every time you wanted to play another game. So let's first understand how a loop works.

What is a Loop?

A loop executes a sequence of statements until a particular condition is true (or false). We can actually write a loop with the C++ statements that we have met so far. We just need an if and the dreaded goto. Look at this example:

// EX2_07.CPP
// Creating a loop with an if and a goto
#include <iostream>
using namespace std;

int main()
{
   int i = 0, sum = 0;
   const int max = 10;
   i = 1;

loop:
   sum += i;             // Add current value of i to sum
   if(++i <= max)
      goto loop;         // Go back to loop until i = 11

   cout << endl
        << "sum = " << sum
        << endl
        << "i = " << i
        << endl;

   return 0;
}

This example accumulates the sum of integers from 1 to 10. The first time through the sequence of statements, i is 1 and is added to sum which starts out as zero. In the if, i is incremented to 2 and, as long as it is less than or equal to max, the unconditional branch to loop occurs and the value of i, now 2, is added to sum. This continues with i being incremented and added to sum each time, until finally, when i is incremented to 11 in the if, the branch back will not be executed. If you run this example, you will get this output:

This shows quite clearly how the loop works. However, it uses a goto and introduces a label into our program, both of which are things we should avoid if possible. We can achieve the same thing, and more, with the for loop. Let's have a go at that now.

Try It Out - Using the for Loop

We can rewrite the last code fragment as a working example using a for loop:

// EX2_08.CPP
// Summing integers with a for loop
#include <iostream>
using namespace std;

int main()
{
   int i = 0, sum = 0;
   const int max = 10;

   for(i = 1; i <= max; i++)   // Loop specification
      sum += i;                // Loop statement

   cout << endl
        << "sum = " << sum
        << endl
        << "i = " << i
        << endl;

   return 0;
}

How It Works

If you compile and run this, you will get exactly the same output as you did in the previous example, but the code is much simpler here. The conditions determining the operation of the loop appear in parentheses after the keyword for. There are three expressions that appear within the parentheses:

  • The first sets i to 1

  • The second determines that the loop statement on the following line is executed as long as i is less than or equal to max

  • The third increments i each iteration

Actually, this loop is not exactly the same as the version in Ex2_07.cpp. You can demonstrate this if you set the value of max to 0 in both programs and run them again. Then, you will find that the value of sum is 1 in Ex2_07.cpp and 0 in Ex2_08.cpp, and the value of i differs too. The reason for this is that the if version of the program always executes the loop at least once, since we don't check the condition until the end. The for loop doesn't do this, because the condition is actually checked at the beginning.

The general form of the for loop is:

for (initializing_expression; test_expression; increment_expression)

loop_statement;

Of course, loop_statement can be a block between braces. The sequence of events in executing the for loop is shown here:

As we have said, the loop statement shown in the diagram can also be a block of statements. The expressions controlling the for loop are very flexible. You can even put multiple expressions for each, separated by the comma operator. This gives you a lot of scope in applying the for loop.

Variations on the for Loop

Most of the time, the expressions in a for loop are used in a fairly standard way:

  • The first initializes one or more loop counters

  • The second tests if the loop should continue

  • The third increments or decrements one or more loop counters

However, you are not obliged to use these expressions in this way and quite a few variations are possible.

The initialization expression in a for loop can also include a declaration for a loop variable. Using our previous example, we could have written the loop to include the declaration for the loop counter i:

for(int i = 1; i <= max; i++)   // Loop specification
   sum += i;                    // Loop statement

Naturally, the original declaration for i would need to be omitted in the program. If you make this change to the last example, you will find that it runs exactly as before, but there is something odd about this. A loop has a scope which extends from the for expression to the end of the body of the loop, which of course can be a block of code between braces, as well as just a single statement. The counter i is now declared within the loop scope, but we are still able to refer to it in the output statement, which is outside the scope of i. This is because a special extension has been allowed for loop counters to extend their scope to the scope enclosing the loop.

In Visual C++ 6.0, a counter which is declared within a for loop expression remains in scope after the loop has finished executing. However, I recommend that you don't write programs that rely on the scope of the counter extending beyond the end of the loop. This is because the final draft of the new ANSI/ISO standard for C++ says that the scope of the loop counter should not extend beyond the end of the loop itself; something which is likely to be implemented by future C++ compilers. If you need to use the value in the counter after the loop has executed, then declare the counter outside the scope of the loop.

You can also omit the initialization expression altogether. If we initialize i appropriately in the declaration, we can write the loop as:

int i = 1;
for( ; i <= max; i++)           // Loop specification
   sum += i;                    // Loop statement

You still need the semicolon that separates the initialization expression from the test condition for the loop. In fact, both semicolons must be in place. If you omit the first semicolon, the compiler will be unable to decide which expression has been omitted.

This flexibility also applies to the contents of the increment expression. For example, we can place the loop statement in the last example inside the increment expression - the loop becomes:

for(i = 1; i <= max; sum += i++);     // The whole loop

We need the semicolon after the closing parentheses, to indicate that the loop statement is now empty. If you omit this, the statement immediately following this line will be interpreted as the loop statement.

Try It Out - Using Multiple Counters

You can use the comma operator to include multiple counters in a for loop. We can show this in operation in the following program:

// EX2_09.CPP
// Using multiple counters to show powers of 2
#include <iostream>
#include <iomanip>
using namespace std;

int main()
{
   long i = 0, power = 0;
   const int max = 10;

   for(i = 0, power = 1; i <= max; i++, power += power)
      // Loop statement
      cout << endl
           << setw(10) << i << setw(10) << power;

   cout << endl;
   return 0;
}

How It Works

We initialize two variables in the initialization section of the for loop, separated by the comma operator, and increment each of them in the increment section. Clearly, you can put as many expressions as you like in each position.

You can even specify multiple conditions, separated by commas, in the test part of the for loop; but only the right-most condition will affect when the loop ends.

Note that the assignments defining the initial values for i and power are expressions, not statements. A statement always ends with a semicolon.

For each increment of i, the value of the variable power is doubled by adding it to itself. This produces the powers of two that we are looking for, and so the program will produce the following output:

The setw() manipulator that we saw in the previous chapter is used to align the output nicely. We have #included iomanip so that we can use setw().

Try It Out - The Infinite for Loop

If you omit the test condition then the value is assumed to be true, so the loop will continue indefinitely unless you provide some other means of exiting from it. In fact, if you like, you can omit all the expressions in the parentheses after for. This may not seem to be very useful, but in fact, quite the reverse is true. You will often come across situations where you want to execute a loop a number of times, but you do not know in advance how many iterations you will need. Have a look at this:

// EX2_10.CPP
// Using an infinite for loop to compute an average
#include <iostream>
using namespace std;

int main()
{
   double value = 0.0; // Value entered stored here
   double sum = 0.0;   // Total of values accumulated here
   int i = 0;          // Count of number of values
   char indicator = 'n';    // Continue or not?

   for(;;)                  // Infinite loop
   {
      cout << endl
           << "Enter a value: ";
      cin >> value;   // Read a value
      ++i;                  // Increment count
      sum += value;         // Add current input to total

      cout << endl
           << "Do you want to enter another value
                                     (enter n to end)? ";
      cin >> indicator;     // Read indicator

      if ((indicator == 'n') || (indicator == 'N'))
         break;             // Exit from loop
   }

   cout << endl
        << "The average of the " << i
        << " values you entered is " << sum/i << "."
        << endl;
   return 0;
}

How It Works

This program will compute the average of an arbitrary number of values. After each value is entered you need to indicate whether you want to enter another value, by entering a single character y or n. Typical output from executing this example is:

After declaring and initializing the variables that we're going to use, we start a for loop with no expressions specified, so there is no provision for ending it here. The block immediately following is the subject of the loop which is to be repeated.

The loop block performs two basic actions:

  • It reads a value

  • It checks whether you want to continue to enter values

The first action within the block is to prompt you for input and then read a value into the variable value. The value that you enter is added to sum and the count of the number of values, i, is incremented. After accumulating the value in sum, you are prompted to enter 'n' if you have finished. The character that you enter is stored in the variable indicator for testing against 'n' or 'N' in the if statement. If neither is found, the loop continues, otherwise a break is executed. The effect of break in a loop is similar to its effect in the context of the switch statement. In this instance, it exits the loop immediately by transferring to the statement following the closing brace of the loop block.

Finally, we output the count of the number of values entered and their average, calculated by dividing sum by i. Of course, i will be promoted to double before the calculation, as you will remember from the our earlier discussion of casting.

The continue Statement

There is another statement, besides break, that is used to affect the operation of a loop: the continue statement. This is written simply as:

continue;

Executing continue within a loop starts the next loop iteration immediately, skipping over any statements remaining in the current iteration. We can demonstrate how this works with the following code fragment:

#include <iostream>
using namespace std;

int main()
{
   int i = 0, value = 0, product = 1;
   for(i = 1; i <= 10; i++)
   {
      cin >> value;
      if(value == 0)           // If value is zero
         continue;             // skip to next iteration
      product *= value;
   }

   cout << "Product (ignoring zeros): " << product
        << endl;
   return 0;
}

This loop reads 10 values with the intention of producing the product of the values entered. The if checks each value entered, and if it is zero, then the continue statement skips to the next iteration. This is so that we don't end up with a zero product if one of the values is zero. Obviously, if a zero value occurred on the last iteration, the loop would end. There are clearly other ways of achieving the same result, but continue provides a very useful capability, particularly with complex loops where you may need to skip to the end of the current iteration from various points in the loop.

The effect of the break and continue statements on the logic of a for loop is illustrated here:

Obviously, in a real situation, the break and continue statements are used with some condition-testing logic to determine when the loop should be exited, or when an iteration of the loop should be skipped. The break and continue statements can also be used with the other kinds of loop which we'll discuss later on in this chapter, where they work in exactly the same way.

Try It Out - Using Other Types in Loops

So far, we have only used integers to count loop iterations. You are in no way restricted as to what type of variable you use to count iterations. Look at this example:

// EX2_11.CPP
// Display ASCII codes for alphabetic characters
#include <iostream>
#include <iomanip>
using namespace std;
int main()
{

   for(char capital='A', small='a'; capital<='Z'; capital++, small++)
      cout << endl
           << "\t" << capital                              // Output capital as character
           << hex << setw(10) << static_cast<int>(capital) // Output capital as hex
           << dec << setw(10) << static_cast<int>(capital) // Output capital as decimal
           << "    " << small                              // Output small as character
           << hex << setw(10) << static_cast<int>(small)   // Output small as hex
           << dec << setw(10) << static_cast<int>(small);  // Output small as decimal

   cout << endl;
   return 0;
}

How It Works

The loop in this example is controlled by the char variable capital which we declare along with the variable small in the initializing expression. We also increment both variables in the increment part, so that the value of capital varies from 'A' to 'Z', and the value of small correspondingly varies from 'a' to 'z'.

The loop contains just one output statement spread over seven lines. The first line:

cout << endl

starts a new line on the screen. On each iteration, after outputting a tab character, the value of capital is displayed three times: as a character, as a hexadecimal value and as a decimal value. We insert the manipulator hex which causes succeeding data values to be displayed as hexadecimal values for the second output of capital, and we then insert the manipulator dec to cause succeeding values to be output as decimal once more. We get the char variable capital to output as a numeric value by casting it to int, using the static_cast<>() which we saw in the last chapter. The value of small is output in a similar way. As a result, the program will generate the following output:

    A        41        65    a        61        97
    B        42        66    b        62        98
    C        43        67    c        63        99
    D        44        68    d        64       100
    E        45        69    e        65       101
    F        46        70    f        66       102
    G        47        71    g        67       103
    H        48        72    h        68       104
    I        49        73    i        69       105
    J        4a        74    j        6a       106
    K        4b        75    k        6b       107
    L        4c        76    l        6c       108
    M        4d        77    m        6d       109
    N        4e        78    n        6e       110
    O        4f        79    o        6f       111
    P        50        80    p        70       112
    Q        51        81    q        71       113
    R        52        82    r        72       114
    S        53        83    s        73       115
    T        54        84    t        74       116
    U        55        85    u        75       117
    V        56        86    v        76       118
    W        57        87    w        77       119
    X        58        88    x        78       120
    Y        59        89    y        79       121
    Z        5a        90    z        7a       122

You can also use a floating point value as a loop counter. An example of a for loop with this kind of counter is:

double a = 0.3, b = 2.5;
for(double x = 0.0; x <= 2.0; x += .25)
   cout << "\n\tx = " << x
        << "\ta*x + b = " << a*x + b;

This calculates the value of a*x+b for values of x from 0.0 to 2.0 in steps of 0.25. However, you need to take care when using a floating point counter in a loop. Many decimal values are not represented exactly in binary floating point, so discrepancies can build up with accumulative values.

The while Loop

A second kind of loop in C++ is the while loop. Where the for loop is primarily used to repeat a statement or a block for a prescribed number of iterations, the while loop will continue as long as a specified condition is true. The general form of the while loop is:

while(condition)
   loop_statement;

where loop_statement will be executed repeatedly as long as the condition expression has the value true. Once the condition becomes false, the program continues with the statement following the loop. Of course, a block of statements between braces could replace the single loop_statement. The logic of the while loop can be represented like this:

Try It Out - Using the while Loop

We could rewrite our program to compute averages (Ex2_10.cpp) to use the while loop:

// EX2_12.CPP
// Using a while loop to compute an average
#include <iostream>
using namespace std;

int main()
{
   double value = 0.0; // Value entered stored here
   double sum = 0.0;   // Total of values accumulated here
   int i = 0;          // Count of number of values
   char indicator = 'y';   // Continue or not?

   while(indicator == 'y') // Loop as long as y is entered
   {
      cout << endl
           << "Enter a value: ";
      cin >> value;        // Read a value

      ++i;                 // Increment count
      sum += value;        // Add current input to total

      cout << endl
           << "Do you want to enter another value
                                      (enter n to end)? ";

      cin >> indicator;             // Read indicator
   }

   cout << endl
        << "The average of the " << i
        << " values you entered is " << sum/i << "."
        << endl;
   return 0;
}

How It Works

For the same input, this version of the program will produce the same output as before. One statement has been updated, and another has been added - they are highlighted above. The for loop statement has been replaced by the while statement and the test for indicator in the if has been deleted, as this function is performed by the while condition. You need to initialize indicator with 'y' in place of the 'n' which appeared previously - otherwise the while loop will terminate immediately. As long as the condition in the while returns true, then the loop continues. You can put any expression resulting in true or false as the loop condition. The example would be a better program if the loop condition were extended to allow 'Y' to be entered to continue the loop as well as 'y'. Modifying the while to the following:

while((indicator=='y') || (indicator=='Y'))

would do the trick.

You can also create an infinite while loop by using a condition that is always true. This can be written as follows:

while(1)
{
...
}

Here, as elsewhere, the integer value 1 is converted to the bool value true. Naturally, the same requirement applies here as in the case of the infinite for loop: namely, that there must be some way of exiting the loop within the loop block.

The do-while Loop

The do-while loop is similar to the while loop in that the loop continues as long as the specified loop condition remains true. The main difference is that the condition is checked at the end of the loop-in the case of the while loop and the for loop, the condition is checked at the beginning of the loop. Thus, the do-while loop statement is always executed at least once. The general form of the do-while loop is:

do
{
   loop_statements;
}while(condition);

The logic of this form of loop is shown here:

We could replace the while loop in the last version of the program to calculate an average with a do-while loop:

do
   {
      cout << endl
           << "Enter a value: ";
      cin >> value;         // Read a value
      ++i;                  // Increment count
      sum += value;         // Add current input to total
      cout << "Do you want to enter another value
                                     (enter n to end)?";
      cin >> indicator;     // Read indicator
   } while((indicator=='y') || (indicator=='Y'));

There's little to choose between them, except that this version doesn't depend on the initial value set in indicator for correct operation. As long as you want to enter at least one value, which is not unreasonable for the calculation in question, this version of the loop is preferable.

Nested Loops

You can nest one loop inside another. This technique typically applied to repeating actions at different levels of classification. An example might be calculating the total marks for each student in a class, then repeating the process for each class in a school.

Try It Out - Nested Loops

We can illustrate the effects of nesting one loop inside another by calculating a simple formula. A factorial of an integer is the product of all the integers from 1 to the integer in question; so the factorial of 3, for example, is 1 times 2 times 3, which is 6. The following program will compute the factorial of integers that you enter (until you've had enough):

// EX2_13.CPP
// Demonstrating nested loops to compute factorials
#include <iostream>
using namespace std;

int main()
{
   char indicator = 'n';
   long value = 0,
        factorial = 0;

   do
   {
      cout << endl
           << "Enter an integer value: ";
      cin >> value;
      factorial = 1;

      for(int i = 2; i<=value; i++)
         factorial *= i;

      cout << "Factorial " << value << " is " << factorial;

      cout << endl
           << "Do you want to enter another value
                                        (y or n)? ";
      cin >> indicator;
   } while((indicator=='y') || (indicator=='Y'));

   return 0;
}

How It Works

If you compile and execute this example, the typical output produced is:

Factorial values grow very fast. In fact, 12 is the largest input value for which this example produces a correct result. If you run it with larger input values, leading digits will be lost in the result stored in the variable factorial, and you may well get negative values for the factorial.

This situation doesn't cause any error messages, so it is of paramount importance that you are sure that the values you're dealing with in a program can be contained in the permitted range of the type of variable you're using. You also need to consider the effects of incorrect input values. Errors of this kind, which occur silently, can be very hard to find.

The outer of the two nested loops is the do-while loop, which controls when the program ends. As long as you keep entering y or Y at the prompt, the program will continue to calculate factorial values. The factorial for the integer entered is calculated in the inner for loop. This is executed value times to multiply the variable factorial (with an initial value of 1) with successive integers from 2 to value.

Try It Out - Another Nested Loop

If you haven't dealt much with nested loops they can be a little confusing, so let's try another example. This program will generate a multiplication table of a given size:

// EX2_14.CPP
// Using nested loops to generate a multiplcation table
#include <iostream>
#include <iomanip>
using namespace std;

int main()
{
   const int size = 12;          // Size of table
   int i = 0, j = 0;             // Loop counters

   cout << endl                  // Output table title
        << size << " by " << size
        << " Multiplication Table" << endl << endl;
   cout << endl << "    |";

   for(i=1; i<=size; i++)        // Loop to output column headings
      cout << setw(3) << i << "  ";

   cout << endl;                 // Newline for underlines

   for(i=0; i<=size; i++)
      cout << "_____";           // Underline each heading

   for(i=1; i<=size; i++)        // Outer loop for rows
   {
      cout << endl
           << setw(3) << i << " |";  // Output row label

      // Inner loop to output the rest of the row
      for(j=1; j<=size; j++)
         cout << setw(3) << i*j << "  ";
      // End of inner loop
   }                             // End of outer loop

   cout << endl;
   return 0;
}

How It Works

If you build this example and execute it, you will see the output shown in the figure below. This shows the output window when execution is complete:

The table title is produced by the first output statement in the program. The next output statement, combined with the loop following it, generates the column headings. Each column will be five characters wide, so the heading value is displayed in a field width of three specified by the setw(3) manipulator, followed by two blanks. The output statement preceding the loop outputs four spaces and a vertical bar above the first column that will contain the row headings. A series of underline characters is then displayed beneath the column headings.

The nested loop generates the main table contents. The outer loop repeats once for each row, so i is the row number. The output statement,

      cout << endl
           << setw(3) << i << " |";  // Output row label

goes to a new line for the start of a row and then outputs the row heading given by the value of i in a field width of three, followed by a space and a vertical bar.

A row of values is generated by the inner loop:

      // Inner loop to output the rest of the row
      for(j=1; j<=size; j++)
         cout << setw(3) << i*j << "  ";
      // End of inner loop

This loop outputs values i*j corresponding to the product of the current row value i, and each of the column values in turn by varying j from 1 to size. So, for each iteration of the outer loop, the inner loop executes size iterations. The values are positioned in the same way as the column headings.

When the outer loop is completed, the return is executed to end the program.


(Summary)

Summary

In this chapter, we've assembled all of the essential mechanisms for making decisions in C++ programs. We've also gone through all the facilities for repeating a group of statements. The essentials of what we've discussed are as follows:

  • The basic decision-making capability is based on the set of relational operators, which allow expressions to be tested and compared, and yield a bool value true or false.

  • We can also make decisions based on conditions which return non-bool values. Any non-zero value will be interpreted as true when a condition is tested, while zero is interpreted as false.

  • The primary decision-making capability in C++ is provided by the if statement. Further flexibility is provided by the switch statement, and by the conditional operator.

  • There are three basic methods provided for repeating a block of statements: the for loop, the while loop and the do-while loop. The for loop allows the loop to repeat a given number of times. The while loop allows a loop to continue as long as a specified condition returns true. Finally, do-while executes the loop at least once and allows continuation of the loop as long as a specified condition returns true.

  • Any kind of loop may be nested within any other kind of loop.

  • The keyword continue allows you to skip the remainder of the current iteration in a loop and go straight to the next iteration.

  • The keyword break provides an immediate exit from a loop. It also provides an exit from a switch at the end of a group of case statements.


(Arrays, Pointers and References)

Chapter 3 - Arrays, Pointers and References

So far, we've covered all the basic data types of consequence and have accumulated a basic knowledge of how to perform calculations and make decisions in a program. This chapter is about broadening the application of the basic programming techniques that we have covered so far, from using single data elements to working with whole collections of data items. In this chapter, you will learn:

  • What an array is and how you can use it

  • How to declare and initialize arrays of different types

  • How to declare and use multidimensional arrays

  • What a pointer is and how you can use it

  • How to declare and initialize pointers of different types

  • The relationship between arrays and pointers

  • What a reference is, how it is declared, and some initial ideas on its uses

  • How to create and allocate memory for variables dynamically


(Handling Multiple Data Values of the Same Type)

Handling Multiple Data Values of the Same Type

We already know how to declare and initialize variables of various types, which each hold a single item of information and which we will refer to as data elements. We know how to create a single character in a char variable, a single integer in a variable of type int or of type long, or a single floating point number in a variable of type float or double. The most obvious extension to these ideas is to be able to reference several data elements of a particular type with a single variable name. This would enable you to handle applications of a much broader scope.

Let's think about an example of where you might need this. Suppose that you needed to write a payroll program. Using a separately-named variable for each individual's pay, tax liability, and so on, would be an uphill task to say the least. A much more convenient way to handle such a problem would be to reference an employee by some kind of generic name - EmployeeName to take an imaginative example - and to have other generic names for the kinds of data related to each employee, such as Pay, Tax, and so on. Of course, you would also need some means of picking out a particular employee from the whole bunch, together with the data from the generic variables associated with them. This kind of requirement arises with any collection of like entities that you want to handle in your program, whether they're baseball players or battleships. Naturally, C++ provides you with a way to deal with this.

Arrays

The basis for the solution to all of these problems is provided by the array in C++. An array is simply a number of memory locations, each of which can store an item of data of the same data type, and which are all referenced through the same variable name. The employee names in a payroll program could be stored in one array, the pay for each employee in another, and the tax due for each employee could be stored in a third array.

Individual items in an array are specified by an index value which is simply an integer representing the sequence number of the elements in the array, the first having the sequence number 0, the second 1, and so on. You can also envisage the index value of an array element as an offset from the first element in an array. The first element has an offset of 0 and therefore an index of 0, and an index value of 3 will refer to the fourth element of an array. For our payroll, we could arrange our arrays so that if an employee's name was stored in the EmployeeName array at a given index value, then the arrays Pay and Tax would store the associated data on pay and tax for the same employee in the array positions referenced by the same index value.

The basic structure of an array is illustrated in the figure below:

This shows an array, called Height, with six elements, each storing a different value. These might be the heights of the members of a family, for instance, recorded to the nearest inch. As there are six elements, the index values run from 0 through 5. To refer to a particular element, you write the array name, followed by the index value of the particular element between square brackets. The third element is referred to as Height[2], for example.

The amount of memory required to store each element will be determined by its type, and all the elements of an array will be stored in a contiguous block of memory.

Declaring Arrays

You declare an array in essentially the same way as you declared the variables that we have seen up to now, the only difference being that the number of elements in the array is specified between square brackets immediately following the array name. For example, we could declare the integer array Height, shown in the previous figure, with the following declaration statement:

long Height[6];

Remember that the indexes in an array start at 0, however, so that the highest element of this array is Height[5].

Since each long value occupies 4 bytes in memory, the whole array requires 24 bytes. Arrays can be of any size, subject to the constraints imposed by the amount of memory in the computer that your program will be running on.

You can declare arrays to be of any type. For example, to declare arrays intended to store the capacity and power output of a series of engines, you could write the following:

double cubic_inches[10];
double horsepower[10];

If auto mechanics is your thing, this would enable you to store the cubic capacity and power output of up to 10 engines, referenced by index values from 0 to 9. As we have seen before with other variables, you can declare multiple arrays of a given type in a single statement, but in practice it is better to declare variables in separate statements.

Try It Out - Using Arrays

As a basis for an exercise in using arrays, let's imagine that we have kept a record of both the amount of gas we have bought for the car and the odometer reading on each occasion. We can write a program to analyze this data to see how the gas consumption looks on each occasion that we bought gas:

// Ex3_01.cpp
// Calculating gas mileage
#include <iostream>
#include <iomanip>
using namespace std;

int main()
{
   const int MAX = 20;           // Maximum number of values
   double gas[ MAX ];            // Gas quantity in gallons
   long miles[ MAX ];            // Odometer readings
   int count = 0;                // Loop counter
   char indicator = 'y';         // Input indicator

   while( (indicator == 'y' || indicator == 'Y') && count < MAX )
   {
      cout << endl
           << "Enter gas quantity: ";
      cin >> gas[count];         // Read gas quantity
      cout << "Enter odometer reading: ";
      cin >> miles[count];       // Read odometer value
      ++count;
      cout << "Do you want to enter another(y or n)? ";
      cin >> indicator;
   }

   if(count <= 1)       // count = 1 after 1 entry completed -
   {                    // we need at least 2.
      cout << endl
           << "Sorry - at least two readings are necessary.";
      return 0;
   }

   // Output results from 2nd entry to last entry
   for(int i=1; i < count; i++)
      cout << endl
           << setw(2) << i << "."       // Output sequence number
           << "Gas purchased = " << gas[ i] << " gallons"
                                        // Output gas
           << " resulted in "           // Output miles per gallon
           << (miles[i] - miles[i-1])/gas[i]
           << " miles per gallon.";

   cout << endl;
   return 0;
}

How It Works

Since we need to take the difference between two odometer readings to calculate the miles covered for the gas used, we only use the odometer reading from the first pair of input values - we ignore the gas bought in the first instance. The typical output produced by this example is shown in the figure below:

During the second period shown in the output, the traffic must have been really bad - or maybe the parking brake was always on.

The dimensions of the two arrays gas and miles used to store the input data are determined by the value of the constant variable MAX. By changing the value of MAX, you can change the program to accommodate a different maximum numbers of input values. This technique is commonly used to make a program flexible in the amount of information that it can handle. Of course, all the program code must be written to take account of the array dimensions, or of any other parameters being specified by const variables. However, this presents little difficulty in practice, so there's no reason why you should not adopt this approach. We'll also see later how to allocate memory for storing data as the program executes, so that we don't need to fix the amount of memory allocated for data storage in advance.

Inputting the Data

The data values are read in the while loop. Since the loop variable can run from 0 to MAX-1, we haven't allowed the user of our program to enter more values than the array can handle. We initialize the variables count and indicator to 0 and 'y' respectively, so that the while loop is entered at least once. There's a prompt for each input value required and the value is read into the appropriate array element. The element used to store a particular value is determined by the variable count, which is 0 for the first input. The array element is specified in the cin statement by using count as an index, and count is then incremented ready for the next value.

After you enter each value, the program prompts you for confirmation that another value is to be entered. The character entered is read into the variable indicator and then tested in the loop condition. The loop will terminate unless 'y' or 'Y' is entered and the variable count is less than the specified maximum value, MAX.

Once the input loop ends (by whatever means), the value of count contains one more than the index value of the last element entered in each array. (Remember, we increment it after we enter each new element.) This is checked in order to verify that at least two pairs of values were entered. If this wasn't the case, the program ends with a suitable message, since two odometer values are necessary to calculate a mileage value.

Producing the Results

The output is generated in the for loop. The control variable i runs from 1 to count-1, allowing mileage to be calculated as the difference between the current element, miles[i] and the previous element, miles[i-1]. Note that an index value can be any expression evaluating to an integer that represents a legal index for the array in question.

If the value of an index expression lies outside of the range corresponding to legitimate array elements, you will be referencing a spurious data location that may contain other data, garbage, or even program code. If the reference to such an element appears in an expression, you will be using some arbitrary data value in the calculation, which will certainly produce a result that you did not intend. If you are storing a result in an array element using an illegal index value, you will overwrite whatever happens to be in that location. When this is part of your program code, the results will be catastrophic. If you use illegal index values, there are no warnings produced either by the compiler or at run-time. The only way to guard against this is to code your program to prevent it happening.

The output is generated by a single cout statement for all values entered, except for the first. A line number is also generated for each line of output using the loop control variable i. The miles per gallon is calculated directly in the output statement. You can use array elements in exactly the same way as any other variables in an expression.

Initializing Arrays

To initialize an array, the initializing values are enclosed within curly braces in the declaration and placed following an equals sign after the array name. The following statement shows an example of a declaration and initialization of an array:

int cubic_inches[5] = { 200, 250, 300, 350, 400 };

The values in the initializing list correspond to successive index values of the array, so in this case cubic_inches[0] will have the value 200, cubic_inches[1] the value 250, cubic_inches[2] the value 300, and so on.

You mustn't specify more initializing values than there are elements in the list, but you can include fewer. If there are fewer, the values are assigned to successive elements, starting with the first element which has the index 0. The array elements for which you didn't provide an initial value will be initialized with zero. This isn't the same as supplying no initializing list. Without an initializing list, the array elements will contain junk values. Also, if you include an initializing list, there must be at least one initializing value in it, otherwise the compiler will generate an error message. We can illustrate this with the following, rather limited, example.

Try It Out - Initializing an Array

// Ex3_02.cpp
// Demonstrating array initialization
#include <iostream>
#include <iomanip>
using namespace std;

int main()
{
   int value[5] = { 1, 2, 3 };
   int Junk [5];
   cout << endl;

   for(int i=0; i<5; i++)
      cout << setw(12) << value[i];

   cout << endl;

   for(i=0; i<5; i++)
      cout << setw(12) << Junk[i];

   cout << endl;
   return 0;
}

In this example we declare two arrays, the first of which, value, is initialized in part, and the second, Junk, is not initialized at all. The program generates two lines of output, which on my computer look like this:

The second line (corresponding to values of Junk[0] to Junk[4]) may well be different on your computer.

How It Works

The first three values of the array value are the initializing values and the last two have the default value of 0. In the case of Junk, all the values are spurious since we didn't provide any initial values at all. The array elements will contain whatever values were left there by the program which last used these memory locations.

A convenient way to initialize a whole array to zero is simply to specify a single initializing value as 0. For example, the statement,

long data[100] = {0};    // Initialize all elements to zero

declares the array data, with all one hundred elements initialized with 0.

You can also omit the dimension of an array of numeric type, providing you supply initializing values. The number of elements in the array will be determined by the number of initializing values. For example, the array declaration,

int value[] = { 2, 3, 4 };

defines an array with three elements with initial values 2, 3, and 4.

Character Arrays and String Handling

An array of type char is called a character array and is generally used to store a character string. A character string is a sequence of characters with a special character appended to indicate the end of the string. The string terminating character is defined by the escape sequence '\0', and is sometimes referred to as a null character, being a byte with all bits as zero. The representation of a string in memory is shown in the figure below:

This illustrates how a string looks in memory and shows a form of declaration for a string that we will get to in a moment.

Each character in the string occupies one byte, so together with the null character, a string requires a number of bytes that is one greater than the number of characters contained in the string.

We can declare a character array and initialize it with a string literal between quotation marks. For example:

char movie_star[12] = "Paul Newman";

Note that the terminating '\0' will be supplied automatically by the compiler. If you include one explicitly in the string constant, you will end up with two of them. You must, however, include space for the terminating null in the number of elements that you allot to the array.

You can let the compiler work out the length of an initialized array for you, as we saw in the previous illustration. Have a look at the following declaration:

char President[] = "Ulysses Grant";

Because the dimension is unspecified, the compiler will allocate space for enough elements to hold the initializing string, plus the terminating null character. In this case it allocates 14 elements for the array President. Of course, if you want to use this array later for storing a different string, its length (including the terminating null character) must not exceed 14 bytes. In general, it is your responsibility to ensure that the array is large enough for any string you might subsequently want to store.

You may well have heard of Unicode, or seen references to it in the Visual C++ documentation, so we'll just outline it here so that you know what it refers to. For supporting international character sets, a character type wchar_t is supported, which uses 2 bytes for each character. This allows 65,536 different characters to be represented, which makes it possible for all the national characters sets to be given unique 16-bit codes, so they can coexist within a single 2-byte character set. This makes creating applications intended for multinational markets much easier.

The definition of the 2-byte character set which incorporates all national character sets, as well as all other standard technical and publishing symbols, is called Unicode. The MFC provides facilities for using the Unicode character set in your Windows programs. We won't be going into any further detail on Unicode in this book, not because it's difficult - it isn't - but simply because we have to stop somewhere!

String Input

The header file iostream contains definitions of a number of functions for reading characters from the keyboard. The one that we shall look at here is the function getline(), which reads a string into a character array. This is typically used with statements such as this:

const int MAX = 80;
char name[MAX];
...
cin.getline(name, MAX, '\n');

These statements first declare a char array name with MAX elements and then read characters from cin using the function getline(). The source of the data, cin, is written as shown, with a period separating it from the function name. The significance of various parts of the input statement is shown below:

Characters are read from cin until the '\n' (newline or end line character) character is read, or when MAX-1 characters have been read (the terminating '\0' is appended), whichever occurs first. The '\n' character is generated when you press the Return key on your keyboard, and is therefore usually the most convenient character to end input. You can, however, specify something else if you wish by changing the last argument. The '\n' isn't stored in the input array name, but a '\0' is added at the end of input string in the array.

We will learn more about this form of syntax when we discuss classes later on. Meanwhile, just take it for granted and we'll have a go at using it in an example.

Try It Out - Programming With Strings

We now have enough knowledge to write a simple program to read a string, and then count how many characters it contains.

// Ex3_03.cpp
// Counting string characters
#include <iostream>
using namespace std;

int main()
{
   const int MAX = 80;             // Maximum array dimension
   char buffer[MAX];               // Input buffer
   int count = 0;                  // Character count

   cout << "Enter a string of less than 80 characters:\n";
   cin.getline(buffer, MAX, '\n'); // Read a string until \n

   while(buffer[count]!= '\0')
      // Increment count as long as the current character
      // is not null
      count++;

   cout << endl
        << "The string \"" << buffer
        << "\" has " << count << " characters.";

   cout << endl;
   return 0;
}

Typical output from this program is illustrated below:

How It Works

This program declares a character array buffer and reads a character string from the keyboard after displaying a prompt for the input. Reading from the keyboard ends when the user presses Return, or when MAX-1 characters have been read.

A while loop is used to count the number of characters read. The loop continues as long as the current character, referenced with buffer[count], is not '\0'. This sort of checking on the current character (while stepping through an array) is a common technique in C++. The only action in the loop is to increment count for each non-null character.

There is also a library function, strlen(), that can save you the trouble of coding it yourself. If you use it, you need to include the string.h header file in your program.

Finally in our example, the string and the character count is displayed with a single output statement. Note how we need to use the escape character '\"' to output a quote.

Multidimensional Arrays

The arrays that we have defined so far with one index are referred to as one-dimensional arrays. An array can also have more than one index value, in which case it is called a multidimensional array. Suppose we have a field in which we are growing bean plants in rows of 10, and the field contains 12 such rows (so there are 120 plants in all). We could declare an array to record the weight of beans produced by each plant using the following statement:

double beans[12][10];

This declares the two-dimensional array beans, the first index being the row number, and the second index the number within the row. To refer to any particular element requires two indices. For example, we could set the value of the element reflecting the fifth plant in the third row with the following statement:

beans[2][4] = 10.7;

Remember that the index values start from zero, so the index value for the third row is 2 and the index for the fifth plant within the row is 4.

Being successful bean farmers, we might have several identical fields planted with beans in the same pattern. Assuming that we have eight fields, we could use a three-dimensional array, declared thus:

double beans[8][12][10];

This will record production for all of the plants in each of the fields, the leftmost index referencing a particular field. If we ever get to bean farming on an international scale, we will be able to use a four-dimensional array, with the extra dimension designating the country. Assuming that you're as good a salesman as you are a farmer, growing this quantity of beans to keep up with the demand may start to affect the ozone layer.

Arrays are stored in memory such that the rightmost index value varies most rapidly. You can visualize the array data[3][4] as three one-dimensional arrays of four elements each. The arrangement of this array is illustrated below:

Initializing Multidimensional Arrays

To initialize a multidimensional array, you use an extension of the method used for a one-dimensional array. For example, you can initialize a two-dimensional array, data, with the following declaration:

long data[2][4] = {
                     { 1,  2,  3,  5 },
                     { 7, 11, 13, 17 }
                  };

Thus, the initializing values for each row of the array are contained within their own pair of curly braces. Since there are four elements in each row, there are four initializing values in each group, and since there are two rows, there are two groups between braces, each group of initializing values being separated from the next by a comma.

You can omit initializing values in any row, in which case the remaining array elements in the row will be zero. For example, in the declaration,

long data[2][4] = {
                     { 1,  2,  3       },
                     { 7, 11           }
                  };

the initializing values have been spaced out to show where values have been omitted. The elements data[0][3], data[1][2], and data[1][3] have no initializing values and will therefore be zero.

If you wanted to initialize the whole array with zeros you could simply write:

long data[2][4] = {0};

If you are initializing arrays with even more dimensions, remember that you need as many nested braces for groups of initializing values as there are dimensions in the array.

Try It Out - Storing Multiple Strings

We can use a single two-dimensional array to store several strings. We can see how this works with an example:

// Ex3_04.cpp
// Storing strings in an array.
#include <iostream>
using namespace std;

int main()
{
   char stars[6][80] = { "Robert Redford",
                         "Hopalong Cassidy",
                         "Lassie",
                         "Slim Pickens",
                         "Boris Karloff",
                         "Oliver Hardy"
                       };
   int dice = 0;

   cout << endl
        << " Pick a lucky star!"
        << " Enter a number between 1 and 6: ";
   cin >> dice;

   if(dice >= 1 && dice <= 6)        // Check input validity
      cout << endl                   // Output star name
           << "Your lucky star is " << stars[dice-1];
   else
      cout << endl                   // Invalid input
           << "Sorry, you haven't got a lucky star.";

   cout << endl;
   return 0;
}

How It Works

Apart from its incredible inherent entertainment value, the main point of interest in this example is the declaration of the array stars. It is a two-dimensional char array, which can hold up to 6 strings, each of which can be up to 80 characters (including the terminating null character that is automatically added by the compiler). The initializing strings for the array are enclosed between braces and separated by commas.

One disadvantage of using arrays in this way is the memory that is almost invariably left unused. All of our strings are less than 80 characters and the surplus elements in each row of the array are wasted.

You can also let the compiler work out how many strings you have by omitting the first array dimension and declaring it as follows:

char stars[][80] = { "Robert Redford",
                        "Hopalong Cassidy",
                        "Lassie",
                        "Slim Pickens",
                        "Boris Karloff",
                        "Oliver Hardy"
                   };

This will cause the compiler to define the first dimension to accommodate the number of initializing strings that you have specified. Since we have six the result is exactly the same, but it avoids the possibility of an error. Here you can't omit both array dimensions. The rightmost dimension must always be defined.

Note the semicolon at the end of the declaration. It's easy to forget it when there are initializing values for an array.

Where we need to reference a string for output statement, we need only specify the first index value:

cout << endl                   // Output star name
     << "Your lucky star is " << stars[dice-1];

A single index value selects a particular 80-element sub-array, and the output operation will display the contents up to the terminating null character. The index is specified as dice-1 as the dice values are from 1 to 6, whereas the index values clearly need to be from 0 to 5.


(Indirect Data Access)

Indirect Data Access

The variables that we have dealt with so far provide you with the ability to name a memory location in which you can store data of a particular type. The contents of a variable are either entered from an external source, such as the keyboard, or calculated from other values that are entered. There is another kind of variable in C++ which does not store data that you normally enter or calculate, but greatly extends the power and flexibility of your programs. This kind of variable is called a pointer.

What is a Pointer?

Each memory location that you use to store a data value has an address. The address provides the means for your PC hardware to reference a particular data item. A pointer is a variable that stores an address of another variable of a particular type. A pointer has a variable name just like any other variable and also has a type which designates what kind of variables its contents refer to. Note that the type of a pointer variable includes the fact that it's a pointer. A variable that is a pointer which can contain addresses of locations in memory containing values of type int, is of type 'pointer to int'.

Declaring Pointers

The declaration for a pointer is similar to that of an ordinary variable, except that the pointer name has an asterisk in front of it to indicate that it's a variable which is a pointer. For example, to declare a pointer pnumber that points to a variable of type long, you could use the following statement:

long* pnumber;

This declaration has been written with the asterisk close to the type name. If you wish, you can also write it as:

long *pnumber;

The compiler won't mind at all. However, remember that the type of the variable pnumber is 'pointer to long', which is often indicated by placing the asterisk close to the type name.

You can mix declarations of ordinary variables and pointers in the same statement. For example:

long* pnumber, number = 99;

This declares the pointer pnumber of type 'pointer to long' as before, and also declares the variable number, of type long. On balance, it's probably better to declare pointers separately from other variables, otherwise the statement can appear misleading as to the type of the variables declared, particularly if you prefer to place the * adjacent to the type name. The following statements certainly look clearer and putting declarations on separate lines enables you to add comments for them individually, making for a program that is easier to read:

// Declaration and initialization of long variable
long number = 99;
// Declaration of variable of type pointer to long
long* pnumber;

It's a common convention in C++ to use variable names beginning with p to denote pointers. This makes it easier to see which variables in a program are pointers, which in turn can make a program easier to follow.

Let's take an example to see how this works, without worrying about what it's for. We will come on to how this is used very shortly. Suppose we have the long integer variable number, as we declared it above containing the value 99. We also have the pointer, pnumber, of type pointer to long, which we could use to store the address of our variable number. But how can we obtain the address of a variable?

The Address-Of Operator

What we need is the address-of operator, &. This is a unary operator which obtains the address of a variable. It's also called the reference operator, for reasons we will discuss later in this chapter. To set up the pointer that we have just discussed, we could write this assignment statement:

pnumber = &number;            // Store address of number in pnumber

The result of this operation is illustrated below:

You can use the operator & to obtain the address of any variable, but you need a pointer of the same type to store it. If you want to store the address of a double variable for example, the pointer must have been declared as double*, which is type 'pointer to double'.

Using Pointers

Taking the address of a variable and storing it in a pointer is all very well, but the really interesting aspect is how you can use it. Fundamental to using a pointer is accessing the data value in the variable to which a pointer points. This is done using the indirection operator, *.

The Indirection Operator

The indirection operator, *, is used with a pointer to access the contents of the variable to which it points. The name 'indirection operator' stems from the fact that the data is accessed indirectly. It is also called the de-reference operator, and the process of accessing the data in the variable pointed to by a pointer is termed de-referencing the pointer.

One aspect of this operator that can seem confusing is the fact that we now have several different uses for the same symbol, *. It is the multiply operator, the indirection operator, and it is used in the declaration of a pointer. Each time you use *, the compiler is able to distinguish its meaning by the context. When you multiply two variables, A*B for instance, then there's no meaningful interpretation of this expression for anything other than a multiply operation.

Why Use Pointers?

A question that usually springs to mind at this point is, 'Why use pointers at all?' After all, taking the address of a variable you already know and sticking it in a pointer so that you can de-reference it seems like an overhead you can do without. There a several reasons why pointers are important.

First of all, as you will see shortly, you can use pointer notation to operate on data stored in an array, which often executes faster than if you use array notation. Secondly, when we get to define our own functions later in the book, you will see that pointers are used extensively for enabling access to large blocks of data, such as arrays within a function, that are defined outside. Thirdly and most importantly, you will also see later that you can allocate space for variables dynamically, that is, during program execution. This sort of capability allows your program to adjust its use of memory depending on the input to the program. Since you don't know in advance how many variables you are going to create dynamically, the only way you can do this is by using pointers - so make sure you get the hang of this bit.

Try It Out - Using Pointers

We can try out various aspects of pointer operations with an example:

//Ex3_05.cpp
// Exercising pointers
#include <iostream>
using namespace std;

int main()
{
   long* pnumber = NULL; // Pointer declaration & initialization
   long number1 = 55, number2 = 99;
   pnumber = &number1;   // Store address in pointer
   *pnumber += 11;       // Increment number1 by 11

   cout << endl
        << "number1 = " << number1
        << "   &number1 = " << hex << pnumber;

   pnumber = &number2;   // Change pointer to address of number2
   number1 = *pnumber*10; // 10 times number2

   cout << endl
        << "number1 = " << dec << number1
        << "   pnumber = " << hex << pnumber
        << "   *pnumber = " << dec << *pnumber;

   cout << endl;
   return 0;
}

On my computer, this example generates the following output:

How It Works

There is no input to this example. All operations are carried out with the initializing values for the variables. After storing the address of number1 in the pointer pnumber, the value of number1 is incremented indirectly through the pointer in this statement:

*pnumber += 11;                // Increment number1 by 11

Note that when we first declared the pointer pnumber, we initialized it to NULL. We'll discuss pointer initialization in the next section.

The indirection operator determines that we are adding 11 to the contents of the variable pointed to, that is, number1. If we forgot the *, we would be attempting to add 11 to the address stored in the pointer.

The values of number1 and the address of number1, stored in pnumber, are displayed. We use the hex manipulator to generate the address output in hexadecimal notation.

You can obtain the value of ordinary integer variables as hexadecimal output by using the manipulator hex. You send it to the output stream in the same way that we have applied endl, with the result that all following output will be in hexadecimal notation. If you want the following output to be decimal, you need to use the manipulator dec in the next output statement to switch the output back to decimal mode again.

After the first line of output, the contents of pnumber are set to the address of number2. The variable number1 is then changed to the value of 10 times number2:

number1 = *pnumber*10;                // 10 times number2

This is calculated by accessing the contents of number2 indirectly through the pointer. The second line of output shows the results of these calculations

The address values you see in your output may well be different from those shown in the screenshot above, since they reflect where the program is loaded in memory, which depends on how your operating system is configured. The 0x prefixing the address values indicates that they are hexadecimal numbers. Note that the addresses &number1 and pnumber (when it contains &number2) differ by four bytes. This shows that number1 and number2 occupy adjacent memory locations, as a long variable requires four bytes. The output demonstrates that everything is working as we expect.

Initializing Pointers

Just as with arrays, using pointers that aren't initialized is extremely hazardous. If you do this, you can overwrite random areas of memory. The resulting damage just depends on how unlucky you are, so it's more than just a good idea to initialize your pointers. It's very easy to initialize a pointer to the address of a variable that has already been defined. Here you can see that we have initialized the pointer pnumber with the address of the variable number just by using the operator & with the variable name:

int number = 0;                  // Initialized integer variable
int* pnumber = &number;          // Initialized pointer

When initializing a pointer with another variable, remember that the variable must already have been declared prior to the pointer declaration.

Of course, you may not want to initialize a pointer with the address of a specific variable when you declare it. In this case, you can initialize it with the pointer equivalent of zero. For this, Visual C++ provides the symbol NULL that is already defined as 0, so you can declare and initialize a pointer using the following statement, rather like we did in the last example:

int* pnumber = NULL;        // Pointer not pointing to anything

This ensures that the pointer doesn't contain an address that will be accepted as valid, and provides the pointer with a value that you can check in an if statement, such as:

if(pnumber == NULL)
   cout << endl << "pnumber is null.";

Of course, you can also initialize a pointer explicitly with 0, which will also ensure that it is assigned a value that doesn't point to anything. In spite of it being arguably somewhat less legible, if you expect to run your code with other compilers, it is preferable to use 0 as an initializing value for a pointer that you want to be null.

This is also more consistent with the current 'good practice' in C++, the argument being that if you have an object with a name in C++, it should have a type. However, NULL does not have a type - it's an alias for 0.

To use 0 as the initializing value for a pointer you would simply write:

int* pnumber = 0;                // Pointer not pointing to anything

To check whether a pointer contains a valid address, you could use the statement:

if(pnumber == NULL)              // or pnumber == 0
   cout << endl << "pnumber is null.";

Equally well, you could use the statement,

if(!pnumber)
   cout << endl << "pnumber is null.";

which does exactly the same as the previous example.

Of course, you can also use the form:

if(pnumber!= 0)
   // Pointer is valid, so do something useful

The address pointed to by the NULL pointer contains a junk value. You should never attempt to de-reference a null pointer.

Pointers to char

A pointer of type char* has the interesting property - it can be initialized with a string constant. For example, we can declare and initialize such a pointer with the statement:

char* proverb = "A miss is as good as a mile.";

This looks very similar to initializing a char array but it's slightly different. This will create the string constant (actually a constant array of type char) with the character string appearing between the quotes and terminated with /0, and store the address of the constant in the pointer proverb. This is shown in the figure below:

Try It Out - Lucky Stars With Pointers

We could rewrite our lucky stars example using pointers instead of an array to see how that would work:

// Ex3_06.cpp
// Initializing pointers with strings
#include <iostream>
using namespace std;

int main()
{
   char* pstr1 = "Robert Redford";
   char* pstr2 = "Hopalong Cassidy";
   char* pstr3 = "Lassie";
   char* pstr4 = "Slim Pickens";
   char* pstr5 = "Boris Karloff";
   char* pstr6 = "Oliver Hardy";
   char* pstr  = "Your lucky star is ";
   int dice = 0;

   cout << endl
        << " Pick a lucky star!"
        << " Enter a number between 1 and 6: ";
   cin >> dice;
   cout << endl;

   switch(dice)
   {
      case 1: cout << pstr << pstr1;
              break;
      case 2: cout << pstr << pstr2;
              break;
      case 3: cout << pstr << pstr3;
              break;
      case 4: cout << pstr << pstr4;
              break;
      case 5: cout << pstr << pstr5;
              break;
      case 6: cout << pstr << pstr6;
              break;
      default: cout << "Sorry, you haven't got a lucky star.";
   }

   cout << endl;
   return 0;
}

How It Works

The array in Ex3_04.cpp has been replaced by the six pointers, pstr1 to pstr6, each initialized with a name. We have also declared an additional pointer, pstr, initialized with the phrase that we want to use at the start of a normal output line. Because we have discrete pointers, it is easier to use a switch statement to select the appropriate output message than to use an if as we did in the original version. Any incorrect values that are entered are all taken care of by the default option of the switch.

Outputting the string pointed to by a pointer couldn't be easier. As you can see, you simply write the pointer name. It may cross your mind at this point that in Ex3_05.cpp we wrote a pointer name in the output statement and the address that it contained was displayed. Why is it different here? The answer lies in the way the output operation views a pointer of type 'pointer to char'. It treats a pointer of this type as a string (which is an array of char), and so outputs the string itself, rather than its address.

Using pointers has eliminated the waste of memory that occurred with the array version of this program, but the program seems a little long-winded now - there must be a better way. Indeed there is - using an array of pointers.

Try It Out - Arrays of Pointers

With an array of pointers of type char, each element can point to an independent string, and the lengths of each of the strings can be different. We can declare an array of pointers in the same way that we declare a normal array. Let's go straight to rewriting the previous example using a pointer array:

// Ex3_07.cpp
// Initializing pointers with strings
#include <iostream>
using namespace std;

int main()
{
   // Initializing a pointer array
   char* pstr[] =  { "Robert Redford",
                     "Hopalong Cassidy",
                     "Lassie",
                     "Slim Pickens",
                     "Boris Karloff",
                     "Oliver Hardy"
                   };

   char* pstart = "Your lucky star is ";
   int dice = 0;

   cout << endl
        << " Pick a lucky star!"
        << " Enter a number between 1 and 6: ";
   cin >> dice;
   cout << endl;

   if(dice >= 1 && dice <= 6)           // Check input validity
      cout << pstart << pstr[dice-1];   // Output star name
   else
      // Invalid input
      cout << "Sorry, you haven't got a lucky star.";

   cout << endl;
   return 0;
}

How It Works

In this case, we are nearly getting the best of all possible worlds. We have a one-dimensional array of char pointers declared such that the compiler works out what the dimension should be from the number of initializing strings. The memory usage that results from this is illustrated below:

Compared to using a 'normal' array, the pointer array carries less overhead in terms of space. With an array, we would need to make each row the length of the longest string, and six rows of seventeen bytes each is 102 bytes, so by using a pointer array we have saved a whole... -1 bytes! What's gone wrong? The simple truth is that for this small number of relatively short strings, the size of the extra array of pointers is significant. You would make savings if you were dealing with more strings that were longer and had more variable lengths.

Space saving isn't the only advantage that you get by using pointers. In a lot of circumstances you save time too. Think of what happens if you want to move 'Oliver Hardy' to the first position and 'Robert Redford' to the end. With the pointer array as above you just need to swap the pointers - the strings themselves stay where they are. If we had stored these simply as strings, as we did in Ex3_04.cpp, a great deal of copying would be necessary - we would need to copy the whole string 'Robert Redford' to a temporary location while we copied 'Oliver Hardy' in its place, and then we would need to copy 'Robert Redford' to the end position. This would require significantly more computer time to execute.

Since we are using pstr as the array name, the variable holding the start of the output message needs to be different, so we have called it pstart. We select the string that we want to output by means of a very simple if statement, similar to that in the original version of the example. We either display a star selection or a suitable message if the user enters an invalid value.

One weakness of the way we have written the program is that the code assumes there are six options, even though the compiler is allocating the space for the pointer array from the number of initializing strings that we supply. So if we add a string to the list, we have to alter other parts of the program to take account of this. It would be nice to be able to add strings and have the program automatically adapt to however many strings there are.

The sizeof Operator

A new operator will help us here. The sizeof operator produces an integer constant that gives the number of bytes occupied by its operand. For example, with the variable dice from the previous example, the statement,

cout << sizeof dice;

will output the value 4, since dice was declared as int and therefore occupies 4 bytes.

The sizeof operator can be applied to an element in an array or to the whole array. When the operator is applied to an array name by itself, it produces the number of bytes occupied by the whole array, whereas when it is applied to a single element with the appropriate index value or values, it results in the number of bytes occupied by that element. Thus, in the last example, we could output the number of elements in the pstr array with the expression:

cout << (sizeof pstr)/(sizeof pstr[0]);

The expression divides the number of bytes occupied by the whole pointer array by the number of bytes occupied by the first element of the array. Since all elements of the array occupy the same amount of memory, the result is the number of elements in the array.

Remember that pstr is an array of pointers - using the sizeof operator on the array or on individual elements will not tell us anything about the memory occupied by the text strings.

The sizeof operator can also be applied to a type name rather than a variable, in which case the result is the number of bytes occupied by a variable of that type. The type name should be enclosed between parentheses. For example, after executing the statement,

long_size = sizeof(long);

the variable long_size will have the value 4.

The result returned by sizeof is obviously an integer and you can always treat it as such. Its precise type is actually size_t, which is used for values measured in bytes. The type size_t, which will pop up in various contexts from time to time, is defined in various standard libraries in Visual C++ and is equivalent to unsigned int.

Try It Out - Using the sizeof Operator

We can use this to amend the last example so that it automatically adapts to an arbitrary number of string values from which to select:

// Ex3_08.cpp
// Flexible array management using sizeof
#include <iostream.h>
using namespace std;

int main()
{
   // Initializing a pointer array
   char* pstr[] = { "Robert Redford",
                    "Hopalong Cassidy",
                    "Lassie",
                    "Slim Pickens",
                    "Boris Karloff",
                    "Oliver Hardy"
                  };
   char* pstart = "Your lucky star is ";

   // Number of array elements
   int count = (sizeof pstr)/(sizeof pstr[0]);
   int dice = 0;
   cout << endl
        << " Pick a lucky star!"
        << " Enter a number between 1 and " << count << ": ";
   cin >> dice;
   cout << endl;

   if(dice >= 1 && dice <= count)       // Check input validity
      cout << pstart << pstr[dice-1];   // Output star name
   else
      // Invalid input
      cout << "Sorry, you haven't got a lucky star.";

   cout << endl;
   return 0;
}

How It Works

As you can see, the changes required in the example are very simple. We just calculate the number of elements in the pointer array pstr and store the result in count. Then, wherever the total number of elements in the array was referenced as 6, we just use the variable count. You could now just add a few more names to the list of lucky stars and everything affected in the program will be adjusted automatically.

Constant Pointers and Pointers to Constants

The array pstr in the last example is clearly not intended to be modified in the program, and nor are the strings being pointed to, nor the variable count. It would be a good idea to ensure that these didn't get modified in error in the program. We could very easily protect the variable count from accidental modification by writing this:

const int count = (sizeof pstr)/(sizeof pstr[0]);

However, the array of pointers deserves closer examination. We declared the array like this:

// Initializing a pointer array
   char* pstr[] = { "Robert Redford",
                    "Hopalong Cassidy",
                    "Lassie",
                    "Slim Pickens",
                    "Boris Karloff",
                    "Oliver Hardy"
                  };

Each pointer in the array is initialized with the address of a string literal, "Robert Redford", "Hopalong Cassidy" and so on. VC6 adheres to the ANSI standard, and says that the type of a string literal is 'array of const char'. The reason the compiler allows you to assign a string literal to an array of char* rather than of const char* is for reasons of backwards compatibility with existing code. If you try to alter the character array with a statement like this,

*pstr[0] = 'X';

then the program will crash when you try to run it.

This was not the case with VC5, where the type of a string literal was 'array of char', and the line shown above would have happily let you re-christen Mr. Redford 'Xobert'.

We don't really want to have unexpected behavior like the program crashing at run time, and we can prevent it. A far better way of writing the declaration is as follows:

// Array of pointers to constants
   const char* pstr[] = { "Robert Redford",
                          "Hopalong Cassidy",
                          "Lassie",
                          "Slim Pickens",
                          "Boris Karloff",
                          "Oliver Hardy"
                        };

In this case, there is no ambiguity about the const-ness of the objects pointed to by the elements of the pointer array. If you now attempt to change these objects, the compiler will flag this as an error at compile time.

However, we could still legally write this statement:

pstr[0] = pstr[1];

Those lucky individuals due to be awarded Mr. Redford would get Mr. Cassidy instead, since both pointers now point to the same name. Note that this isn't changing the values of the objects pointed to by the pointer array element - it is changing the value of the pointer stored in pstr[0]. We should therefore inhibit this kind of change as well, since some people may reckon that good old Hoppy does not have the same sex appeal as Robert. We can do this with the following statement:

// Array of constant pointers to constants
   const char* const pstr[] = { "Robert Redford",
                                "Hopalong Cassidy",
                                "Lassie",
                                "Slim Pickens",
                                "Boris Karloff",
                                "Oliver Hardy"
                              };

To summarize, we can distinguish three situations relating to const, pointers and the objects to which they point:

  • A pointer to a constant object

  • A constant pointer to an object

  • A constant pointer to a constant object

In the first situation, the object pointed to cannot be modified, but we can set the pointer to point to something else:

const char* pstring = "Some text";

In the second, the address stored in the pointer can't be changed, but the object pointed to can be:

char* const pstring = "Some text";

Finally, in the third situation, both the pointer and the object pointed to have been defined as constant and, therefore, neither can be changed:

const char* const pstring = "Some text";

Of course, all this applies to pointers of any type. Type char is used here purely for illustrative purposes.

Pointers and Arrays

Array names can behave like pointers under some circumstances. In most situations, if you use the name of a one-dimensional array by itself, it is automatically converted to a pointer to the first element of the array. Note that this is not the case when the array name is used as the operand of the sizeof operator.

If we have these declarations,

double* pdata;
double data[5];

then we can write this assignment:

pdata = data;       // Initialize pointer with the array address

This statement assigns the address of the first element of the array data to the pointer pdata. Using the array name by itself refers to the address of the array, whereas the array name, data, with an index value refers to the contents of the element corresponding to that index value. So, if we want to store the address of that element in the pointer, then we have to use the address-of operator:

pdata = &data[1];

Here, the pointer pdata will contain the address of the second element of the array.

Pointer Arithmetic

You can perform arithmetic operations with pointers. You are limited to addition and subtraction in terms of arithmetic, but you can also perform comparisons using pointers to produce a logical result. Arithmetic with a pointer implicitly assumes that the pointer points to an array, and that the arithmetic operation is on the address contained in the pointer. For the pointer pdata, for example, we could assign the address of the third element of the array data to a pointer with this statement:

pdata = &data[2];

In this case, the expression pdata+1 would refer to the address of data[3], the fourth element of the data array, so we could make the pointer point to this element by writing this statement:

pdata += 1;          // Increment pdata to the next element

This increments the address contained in pdata by the number of bytes occupied by one element of the array, data. In general, the expression pdata+n, where n can be any expression resulting in an integer, will add n*sizeof(double) to the address contained in the pointer pdata, since it was declared to be of type pointer to double. This is illustrated below:

In other words, incrementing or decrementing a pointer works in terms of the type of the object pointed to. Increasing a pointer to long by one changes its contents to the next long address, and so increments the address by four. Similarly, incrementing a pointer to short by one will increment the address by two. The more common notation for incrementing a pointer is using the increment operator. For example, this,

pdata++;            // Increment pdata to the next element

is equivalent to (and more common than) the += form. However, the += form was used above to make it clear that while the increment value is actually specified as one, the effect is usually an increment greater than one, except in the case of a pointer to char.

The address resulting from an arithmetic operation on a pointer can be a value ranging from the address of the first element of the array to the address which is one beyond the last element. Outside of these limits, the behavior of the pointer is undefined.

You can, of course, de-reference a pointer on which you have performed arithmetic (there wouldn't be much point to it otherwise). For example, assuming that pdata is still pointing to data[2], then this statement,

*(pdata+1) = *(pdata+2);

is equivalent to this:

data[3] = data[4];

When you want to de-reference a pointer after incrementing the address it contains, the parentheses are necessary as the precedence of the indirection operator is higher than that of the arithmetic operators, + or -. If you write the expression *pdata+1, instead of *(pdata+1), this would add one to the value stored at the address contained in pdata, which is equivalent to executing data[2]+1. Since this isn't an lvalue, its use in the assignment statement above would cause the compiler to generate an error message.

We can use an array name as though it were a pointer for addressing elements of an array. If we have the same one-dimensional array as before, declared as

long data[5];

then using pointer notation, we can refer to the element data[3] for example as *(data+3). This kind of notation can be applied generally so that, corresponding to the elements data[0], data[1], data[2], we can write *data, *(data+1), *(data+2), and so on.

Try It Out - Array Names as Pointers

We could exercise this aspect of array addressing with a program to calculate prime numbers (a prime number is a number divisible only by itself and one).

// Ex3_09.cpp
// Calculating primes
#include <iostream>
#include <iomanip>
using namespace std;

int main()
{
   const int MAX = 100;        // Number of primes required
   long primes[MAX] = { 2,3,5 };  // First three primes defined
   long trial = 5;             // Candidate prime
   int count = 3;              // Count of primes found
   int found = 0;              // Indicates when a prime is found

   do
   {
      trial += 2;                    // Next value for checking
      found = 0;                     // Set found indicator

      // Try division by existing primes
      for(int i = 0; i < count; i++)
      {
         found = (trial % *(primes+i)) == 0; // True for exact division
         if(found)               // If division is exact
            break;               // it's not a prime
      }

      if (found == 0)        // We got one... so save it in
         *(primes + count++) = trial;   // ... primes array
   }while(count < MAX);

   // Output primes 5 to a line
   for(int i = 0; i < MAX; i++)
   {
      if(i%5 == 0)      // New line on 1st, and every 5th line
         cout << endl;
      cout << setw(10) << *(primes+i);
   }

   cout << endl;
   return 0;
}

If you compile and execute this example you should get the output shown:

How It Works

We have the usual #include statements for iostream for input and output, and for iomanip since we will be using a stream manipulator to set the field width for output.

We use the constant MAX to define the number of primes that we want the program to produce. The primes array, which stores the results, has the first three primes already defined to start the process off. All the work is done in two loops, the outer do-while loop, which picks the next value to be checked and adds the value to the primes array if it is prime, and the inner for loop that actually checks the value to see whether it's prime or not.

The algorithm in the for loop is very simple and is based on the fact that if a number is not a prime, then it must be divisible by one of the primes found so far - all of which are less than the number in question, since all numbers are either prime or a product of primes. In fact, only division by primes less than or equal to the square root of the number in question need to be checked, so this example isn't as efficient as it might be.

found = (trial % *(primes+i)) == 0; // True for exact division

This statement sets the variable found to be 1 if there's no remainder from dividing the value in trial by the current prime, *(primes+i) (remember that this is equivalent to primes[i]), and 0 otherwise. The if statement causes the for loop to be terminated if found has the value 1, since the candidate in trial can't be a prime in that case.

After the for loop ends (for whatever reason), it's necessary to decide whether or not the value in trial was prime. This is indicated by the value in the indicator variable found.

*(primes + count++) = trial;   //    primes array

If trial does contain a prime, this statement stores the value in primes[count] and then increments count through the postfix increment operator.

Once MAX number of primes have been found, they are output with a field width of 10 characters, 5 to a line, as a result of this statement:

if(i%5 == 0)      // New line on 1st, and every 5th line
   cout << endl;

This starts a new line when i has the values 0, 5, 10, and so on.

Try It Out - Counting Characters Revisited

To see how handling strings works in pointer notation, we could produce a version of the program we looked at earlier for counting the characters in a string:

// Ex3_10.cpp
// Counting string characters using a pointer
#include <iostream>
using namespace std;

int main()
{
   const int MAX = 80;                 // Maximum array dimension
   char buffer[MAX];                   // Input buffer
   char* pbuffer=buffer;               // Pointer to array buffer
   cout << endl                        // Prompt for input
        << "Enter a string of less than "
        << MAX << " characters:"
        << endl;
   cin.getline(buffer, MAX, '\n');     // Read a string until \n

   while(*pbuffer)                     // Continue until \0
      pbuffer++;

   cout << endl
        << "The string \"" << buffer
        << "\" has " << pbuffer-buffer << " characters.";

   cout << endl;
   return 0;
}

How It Works

Here the program operates using the pointer pbuffer rather than the array name buffer. We don't need the count variable, since the pointer is incremented in the while loop until \0 is found. When the \0 is found, pbuffer will contain the address of that position in the string. The count of the number of characters in the string entered is therefore the difference between the address stored in the pointer pbuffer and the address of the beginning of the array denoted by buffer.

We could also have incremented the pointer in the loop by writing the loop like this:

while(*pbuffer++);                   // Continue until \0

Now the loop contains no statements, only the test condition. This would work adequately, except for the fact that the pointer would be incremented after \0 was encountered, so the address would be one more than the last position in the string. We would therefore need to express the count of the number of characters in the string as pbuffer-buffer-1.

Note that here we can't use the array name in the same way that we have used the pointer. The expression buffer++ is strictly illegal since you can't modify an array name - it isn't a pointer.

Using Pointers with Multidimensional Arrays

Using a pointer to store the address of a one-dimensional array is relatively straightforward, but with multidimensional arrays, things can get a little complicated. If you don't intend to do this, you can skip this section as it's a little obscure, but if your previous experience is with C, this section is worth a glance.

If you have to use a pointer with multidimensional arrays, you need to keep clear in your mind what is happening. By way of illustration, we can use an array beans, declared as follows:

double beans[3][4];

We can declare and assign a value to the pointer pbeans as follows:

double* pbeans;
pbeans = &beans[0][0];

Here we are setting the pointer to the address of the first element of the array, which is of type double. We could also set the pointer to the address of the first row in the array with the statement:

pbeans = beans[0];

This is equivalent to using the name of a one-dimensional array which is replaced by its address. We used this in the earlier discussion. However, because beans is a two-dimensional array, we cannot set an address in the pointer with the following statement:

pbeans = beans;           // Will cause an error!!

The problem is one of type. The type of the pointer you have defined is double*, but the array is of type double[3][4]. A pointer to store the address of this array must be of type double*[4]. C++ associates the dimensions of the array with its type and the statement above is only legal if the pointer has been declared with the dimension required. This is done with a slightly more complicated notation than we have seen so far:

double (*pbeans)[4];

The parentheses here are essential, otherwise you would be declaring an array of pointers. Now the previous statement is legal, but this pointer can only be used to store addresses of an array with the dimensions shown.

Pointer Notation with Multidimensional Arrays

You can use pointer notation with an array name to reference elements of the array. You can reference each element of the array beans that we declared earlier in two ways:

  • Using the array name with two index values.

  • Using the array name in pointer notation

Therefore, the following two statements are equivalent:

beans[i][j]
*(*(beans+i)+j)

Let's look at how these work. The first line uses normal array indexing to refer to the element with offset j in row i of the array.

We can determine the meaning of the second line by working outwards from the inside. beans refers to the address of the first row of the array, so beans+i refers to row i of the array. The expression *(beans+i) is the address of the first element of row i, so *(beans+i)+j is the address of the element in row i with offset j. The whole expression therefore refers to the value of that element.

If you really want to be obscure - and it isn't recommended that you do so - the following two statements, where we have mixed array and pointer notation, are also legal references to the same element of the array:

*(beans[i]+j)
(*(beans+i))[j]

There is yet another aspect to the use of pointers which is really the most important of all: the ability to create variables dynamically. We will look into that next.


(Dynamic Memory Allocation)

Dynamic Memory Allocation

Working with a fixed set of variables in a program can be very restrictive. The need often arises within an application to decide the amount of space to be allocated for storing different types of variables at execution time, depending on the input data for the program. With one set of data it may be appropriate to use a large integer array in a program, whereas with a different set of input data, a large floating point array may be required. Obviously, since any dynamically allocated variables can't have been defined at compile time, they can't be named in your source program. When they are created, they are identified by their address in memory which is contained within a pointer. With the power of pointers and the dynamic memory management tools in Visual C++, writing your programs to have this kind of flexibility is quick and easy.

The Free Store, Alias the Heap

In most instances, when your program is executed there is unused memory in your computer. This unused memory is called the heap in C++, or sometimes the free store. You can allocate space within the free store for a new variable of a given type using a special operator in C++ which returns the address of the space allocated. This operator is new, and it's complemented by the operator delete, which de-allocates memory previously allocated by new.

You can allocate space in the free store for some variables in one part of a program, and then release the allocated space and return it to the free store once you have finished with the variables concerned. This makes the memory available for reuse by other dynamically allocated variables, later in the same program.

You would want to use memory from the free store whenever you need to allocate memory for items that can only be determined at run time. One example of this might be allocating memory to hold a string entered by the user of your application. There is no way you can know in advance how large this string will need to be, so you would allocate the memory for the string at run time, using the new operator. Later, we'll look at an example of using the free store to dynamically allocate memory for an array, where the dimensions of the array are determined by the user at run time.

This can be a very powerful technique; it enables you to use memory very efficiently, and in many cases, it results in programs that can handle much larger problems, involving considerably more data than otherwise might be possible.

The Operators new and delete

Suppose that we need space for a double variable. We can define a pointer to type double and then request that the memory be allocated at execution time. We can do this using the operator new with the following statements:

double* pvalue = NULL;    // Pointer initialized with null
pvalue = new double;      // Request memory for a double variable

This is a good moment to recall that all pointers should be initialized. Using memory dynamically typically involves a number of pointers floating around, so it's important that they should not contain spurious values. You should try to arrange that if a pointer doesn't contain a legal address value, it is set to NULL.

The new operator in the second line of code above should return the address of the memory in the free store allocated to a double variable, and this address will be stored in the pointer pvalue. We can then use this pointer to reference the variable using the indirection operator as we have seen. For example, we could set the value of the double variable to 9999.0 with the statement:

*pvalue = 9999.0;

However, using a dynamic variable as shown here is very risky. The memory may not have been allocated, if the free store had been used up. Alternatively, it could be that the free store is fragmented by previous usage, so there wasn't a sufficient number of contiguous bytes to accommodate the variable for which you want to obtain space. In this case, the operator new will return a NULL pointer value, so before using it we should always test for a valid address being returned and stored in our pointer. We could have done this by writing the following:

if(!(pvalue = new double))
{
   cout << endl
        << "Out of memory.";
   exit(1);
}

Here, we have called for the space to be allocated and the address to be stored in the pointer, pvalue, all within the if statement. If a NULL pointer value was returned, the if expression will be true, so the message will be displayed and the exit() function called to end the program. The exit() function is used when you want to terminate a program abnormally. The value between the parentheses is an integer (int) value that can be used to indicate the circumstances under which the program was terminated. If you use the exit() function, you should #include the header file cstdlib into your program.

You can also initialize a variable created by new. Taking our example of the double variable which was allocated by new and the address stored in pvalue, we could have set the value to 999.0 as it was created with this statement:

pvalue = new double(999.0); // Allocate double and initialize it

When you no longer need a variable that has been dynamically allocated, you can free up the memory that it occupies in the free store with the delete operator:

delete pvalue;        // Release memory pointed to by pvalue

This ensures that the memory can be used subsequently by another variable. If you don't use delete, and subsequently store a different address value in the pointer pvalue, it will be impossible to free up the memory or to use the variable that it contains, since access to the address will have been lost.

Allocating Memory Dynamically for Arrays

Allocating memory for an array dynamically is very straightforward. If we wanted to allocate an array of type char, assuming pstr is a pointer to char, we could write the following statement:

pstr = new char[20];  // Allocate a string of twenty characters

This allocates space for a char array of 20 characters and stores its address in pstr.

To remove the array that we have just created in the free store, we must use the delete operator. The statement would look like this:

delete [] pstr;          // Delete array pointed to by pstr

Note the use of square brackets to indicate that what we are deleting is an array. When removing arrays from the free store, you should always include the square brackets or the results will be unpredictable. Note also that you do not specify any dimensions here, simply [].

Try It Out - Using Free Store

We can see how this works in practice by rewriting our program to calculate an arbitrary number of primes, but this time using memory in the free store to store them.

// Ex3_11.cpp
// Calculating primes using dynamic memory allocation
#include <iostream>
#include <iomanip>
#include <cstdlib>           // For the exit function
using namespace std;
int main()
{
   long* pprime=0;           // Pointer to prime array
   long trial = 5;           // Candidate prime
   int count = 3;            // Count of primes found
   int found = 0;            // Indicates when a prime is found
   int max = 0;              // Number of primes required
   cout << endl
        << "Enter the number of primes you would like: ";
   cin >> max;               // Number of primes required
   if(!(pprime=new long[max]))
   {
      cout << endl
           << "Memory allocation failed.";
      exit(1);               // Terminate program
   }
   *pprime = 2;              // Insert three
   *(pprime+1) = 3;          // seed primes
   *(pprime+2) = 5;
   do
   {
      trial += 2;            // Next value for checking
      found = 0;             // Set found indicator
      for(int i = 0; i < count; i++)
                             // Division by existing primes
      {
         found =(trial % *(pprime+i)) == 0; // True for exact
                                                      division
         if(found)           // If division is exact
            break;           // it's not a prime
      }
      if (found == 0)        // We got one... so save it in
         *(pprime+count++) = trial;         // primes array
   } while(count < max);

   // Output primes 5 to a line
   for(int i = 0; i < max; i++)
   {
      if(i%5 == 0)      // New line on 1st, and every 5th line
         cout << endl;
      cout << setw(10) << *(pprime+i);
   }
   delete [] pprime;             // Free up memory
   cout << endl;
   return 0;
}

How It Works

Apart for the prompt for the number of primes required, the output from this example is the same as the previous version (assuming that the same number of primes is being generated), so we won't reproduce it again here.

In fact, the program is very similar to the previous version. We have an extra #include statement for cstdlib, because we are using the function exit() if we run out of memory. After receiving the number of primes required in the int variable max, we allocate an array of that size in the free store using the operator new. We specify the size of the array required by putting the variable max between the square brackets following the array type specification. The pointer value, returned by new and stored in the pointer pprime, is validated in the if statement. If it turns out to be NULL, a message is displayed and the program is exited.

if(!(pprime=new long[max]))
{
   cout << endl
        << "Memory allocation failed.";
   exit(1);                   // Terminate program
}

Assuming that the memory allocation is successful, the first three array elements are set to the values of the first three primes.

We can't specify initial values for elements of an array allocated dynamically. We have to use explicit assignment statements if we want to set initial values for elements of the array.

The calculation of the prime numbers is exactly as before; the only change is that the name of the pointer we have here, pprime, is substituted for the array name primes, used in the previous version. Equally, the output process is the same. Acquiring space dynamically is really not a problem at all. Once it has been allocated, it in no way affects how the computation is written.

Once we have finished with the array, we remove it from the free store using the delete operator, not forgetting to include the square brackets to indicate that it is an array we are deleting.

delete [] pprime;             // Free up memory

Dynamic Allocation of Multidimensional Arrays

Allocating memory in the free store for a multidimensional array involves using the operator new in only a slightly more complicated form than that for a one-dimensional array. Assuming that we have already declared the pointer pbeans appropriately, to obtain the space for our array beans[3][4] that we used earlier in this chapter, we could write this:

pbeans = new double [3][4];  // Allocate memory for a 3x4 array

Allocating space for a three-dimensional array simply requires the extra dimension specified with new, as in this example:

// Allocate memory for a 5x10x10 array
pBigArray = new double [5][10][10];

However many dimensions there are in the array that has been created, to destroy it and release the memory back to the free store, you write the following:

delete [] pBigArray;         // Release memory for array

You use just one pair of square brackets regardless of the dimensionality of the array with which you are dealing.

We have already seen that we can use a variable as the specification of the dimension of a one-dimensional array to be allocated by new. This extends to two or more dimensions only in that the leftmost dimension may be specified by a variable. All the other dimensions must be constants or constant expressions. So we could write this,

pBigArray = new double[max][10][10];

where max is a variable. However, specifying a variable for any other dimension will cause an error message to be generated by the compiler.


(Using References)

Using References

A reference appears to be similar to a pointer in many respects, which is why we've introduced it here, but it isn't the same thing at all. Its real significance will only become apparent when we get to discuss its use with functions, particularly in the context of object-oriented programming. Don't be misled by its simplicity and what might seem to be a trivial concept. As you will see later, references provide some extraordinarily powerful facilities, and in some contexts it will enable you to achieve results that would be impossible without using them.

What is a Reference?

A reference is an alias for another variable. It has a name that can be used in place of the original variable name. Since it is an alias, and not a pointer, the variable for which it is an alias has to be specified when the reference is declared, and unlike a pointer, a reference can't be altered to represent another variable.

Declaring and Initializing References

If we have a variable declared as follows,

long number = 0;

we can declare a reference for this variable using the following declaration statement:

long& rnumber = number; // Declare a reference to variable number

The ampersand following the type long and preceding the name rnumber, indicates that a reference is being declared, and the variable name it represents, number, is specified as the initializing value following the equals sign. Therefore, rnumber is of type 'reference to long'. The reference can now be used in place of the original variable name. For example, this statement,

rnumber += 10;

has the effect of incrementing the variable number by 10.

Let's contrast the reference rnumber with the pointer pnumber, declared in this statement:

long* pnumber = &number; // Initialize a pointer with an address

This declares the pointer pnumber, and initializes it with the address of the variable number. This then allows the variable number to be incremented with a statement such as:

*pnumber += 10;         // Increment number through a pointer

You should see a significant distinction between using a pointer and using a reference. The pointer needs to be de-referenced and whatever address it contains is used to access the variable to participate in the expression. With a reference there is no need for de-referencing. In some ways, a reference is like a pointer that has already been de-referenced, although it can't be changed to reference another variable. The reference is the complete equivalent of the variable for which it is a reference. A reference may seem like just an alternative notation for a given variable, and here it certainly appears to behave like that. However, we shall see when we come to discuss functions in C++ that this is not quite true, and that it can provide some very impressive extra capabilities.


(Summary)

Summary

You are now familiar with all of the basic types of values in C++, how to create and use arrays of those types and how to create and use pointers. You have also been introduced to the idea of a reference. However, we have not exhausted all of these topics. We'll come back to arrays, pointers and references again later on. The important points that we have discussed in this chapter are:

  • An array allows you to manage a number of variables of the same type using a single name. Each dimension of an array is defined between square brackets following the array name in the declaration of the array.

  • Each dimension of an array is indexed starting from zero. Thus the fifth element of a one-dimensional array will have the index value 4.

  • Arrays can be initialized by placing the initializing values between curly braces in the declaration.

  • A pointer is a variable that contains the address of another variable. A pointer is declared as a 'pointer to type', and may only be assigned addresses of variables of the given type.

  • A pointer can point to a constant object. Such a pointer can be reassigned to another object. A pointer may also be defined as const, in which case it can't be reassigned.

  • A reference is an alias for another variable, and can be used in the same places as the variable it references. A reference must be initialized in its declaration.

  • A reference can't be reassigned to another variable.

  • The operator sizeof returns the number of bytes occupied by the object specified as its argument. Its argument may be a variable or a type name between parentheses.

  • The operator new allocates memory dynamically in the free store. When memory has been assigned as requested, it returns a pointer to the beginning of the memory area provided. If memory cannot be assigned for any reason, a NULL pointer will be returned. Memory allocated by new can only be freed using the delete operator with the address originally returned by new as an argument.

The pointer mechanism is sometimes a bit confusing because it can operate at different levels within the same program. Sometimes it is operating as an address, and at other times it can be operating with the value stored at an address. It's very important that you feel at ease with the way pointers are used, so if you find that they are in any way unclear, try them out with a few examples of your own until you feel confident about applying them.


(Introducing Structure Into Your Programs)

Chapter 4 - Introducing Structure Into Your Programs

Up to now we haven't really been able to structure our program code in a modular fashion, since we've only been able to construct a program as a single function, main(). We have, however, been using library functions of various kinds. Whenever you write a C++ program, you should have a modular structure in mind from the outset and, as we shall see, a good understanding of how to implement functions is essential to object-oriented programming in C++. In this chapter, you'll learn:

  • How to declare and write your own C++ functions

  • What function arguments are, and how they are defined and used

  • How arrays can be passed to and from a function

  • What pass-by-value means

  • How to pass pointers to functions

  • How to use references as function arguments, and what pass-by-reference means

  • How the const modifier affects function arguments

  • How to return values from a function

  • What recursion is and how it can be used

There's quite a lot to structuring your C++ programs, so to avoid indigestion, we won't try to swallow the whole thing in one gulp. Once we've chewed over and gotten the full flavor of the morsels listed above, we'll move on to the next chapter, where we shall get further into the meat of the topic.


(Understanding Functions)

Understanding Functions

Let's first look at the broad principles of how a function works. A function is a self-contained block of code with a specific purpose. A function has a name that both identifies it and is used to call it for execution in a program. The name of a function is global, but is not necessarily unique in C++, as we shall see in the next chapter. However, functions which perform different actions should generally have different names.

The name of a function is governed by the same rules as those for a variable. A function name is, therefore, a sequence of letters and digits, the first of which is a letter, and where an underscore counts as a letter. The name of a function should generally reflect what it does, so for example, you might call a function that counts beans CountBeans().

You pass information to a function by means of arguments specified when you invoke it. These arguments need to correspond with parameters appearing in the definition of the function. The arguments that you specify replace the parameters used in its definition when the function is executed. The code in the function then executes as though it was written using your argument values. The relationship between arguments in the function call and its parameters is illustrated below:

In this example, the function returns the sum of the two arguments passed to it. In general, a function returns either a single value to the point in the program where it was called, or nothing at all, depending on how the function is defined. You might think that returning a single value from a function is a constraint, but the single value returned can be a pointer which might contain the address of an array, for example. We will see more about how data is returned from a function a little later in this chapter.

Why Do You Need Functions?

One major advantage that a function offers is that it can be executed as many times as necessary from different points in a program. Without the ability to package a block of code into a function, programs would end up being much larger, since you would typically need to replicate the same code at various points in them. But the real reason that you need functions is to break up a program into easily manageable chunks.

Imagine a really big program, a million lines of code let's say. A program of this size would be virtually impossible to write without functions. Functions allow a program to be segmented so that it can be written piecemeal, and each piece tested independently before bringing it together with the other pieces. It also allows the work to be divided among members of a programming team, with each team member taking responsibility for a tightly specified piece of the program, with a well-defined functional interface to the rest of the code.

Structure of a Function

As we have seen when writing the function main(), a function consists of a function header which identifies the function, followed by the body of the function between curly braces containing the executable code for the function. Let's look at an example. We could write a function to raise a value to a given power, that is, compute xn:

double power(double x, int n)    // Function header
{                                // Function body starts here...
   double result = 1.0;          // Result stored here
   for(int i = 1; i<=n; i++)
      result *= x;
   return result;
}                                // ...and ends here

The Function Header

Let's first examine the function header in this example. This is the first line of the function:

double power(double x, int n)    // Function header

It consists of three parts:

  • The type of the return value (double in this case)

  • The name of the function, power

  • The parameters of the function enclosed between parentheses

The return value is returned to the calling function when the function is executed, so when the function is called, it will have a value of type double in the expression in which it appears.

Our function has two parameters: x, the value to be raised to a given power, which is of type double; and the value of the power, n, which is of type int. The computation that the function performs is written using these parameter variables together with another variable, result, declared in the body of the function.

Note that no semicolon is required at the end of the function header.

The General Form of a Function Header

The general form of a function header can be written as follows:

return_type FunctionName(parameter_list)

The return_type can be any legal type. If the function does not return a value, the return type is specified by the keyword void. The keyword void is also used to indicate the absence of parameters, so a function that has no parameters and doesn't return a value would have this header:

void MyFunction(void)

An empty parameter list also indicates that a function takes no arguments, so you could omit the keyword void between the parentheses as follows:

void MyFunction()

A function with a return type specified as void should not be used in an expression in the calling program. Because it doesn't return a value, it can't sensibly be part of an expression, so using it in this way will cause the compiler to generate an error message.

The Function Body

The desired computation in a function is performed by the statements in the function body following the function header. In our example, the first of these declares a variable result which is initialized with the value 1.0. The variable result is local to the function, as are all automatic variables declared within a function body. This means that the variable result ceases to exist after the function has completed execution.

The calculation is performed in the for loop. A loop control variable i is declared in the for loop which will assume successive values from 1 to n. The variable result is multiplied by x once for each loop iteration, so this occurs n times to generate the required value. If n is 0, the statement in the loop won't be executed at all because the loop continuation condition will immediately fail, and so result will be left as 1.0.

As we've said, all the variables declared within the body of a function, as well as the parameters, are local to the function. There is nothing to prevent you from using the same names for variables in other functions for quite different purposes. Indeed, it's just as well this is so, because it would be extremely difficult to ensure variable names were always unique within a program containing a large number of functions, particularly if the functions were not all written by the same person.

The scope of variables declared within a function is determined in the same way that we discussed in the first chapter. A variable is created at the point at which it is defined and ceases to exist at the end of the block containing it. There is one type of variable that is an exception to this - those declared as static. We'll discuss static variables a little later in this chapter.

Be careful about masking global variables with local variables of the same name. We discussed this situation back in Chapter 2 and saw how we could use the scope resolution operator :: to avoid any problems.

The return Statement

The return statement returns the value of result to the point where the function was called. What might immediately strike you is that we just said result ceases to exist on completing execution of the function - so how is it returned? The answer is that a copy of the value being returned is made automatically, and this copy is available to the return point in the program.

The general form of the return statement is as follows,

return expression;

where expression must evaluate to a value of the type specified in the function header for the return value. The expression can be any expression you want, as long as you end up with a value of the required type. It can include function calls - even a call of the same function in which it appears, as we shall see later in this chapter.

If the type of return value has been specified as void, there must be no expression appearing in the return statement. It must be written simply as:

return;

Using a Function

Before you can use a function in a program, you must declare the function using a statement called a function prototype.

Function Prototypes

A prototype of a function provides the basic information that the compiler needs, to check that a function is used correctly. It specifies the parameters to be passed to the function, the function name and the type of the return value - essentially, it contains the same information as appears in the function header, with the addition of a semicolon. Clearly, the number of parameters and their types must be the same in the function prototype as they are in the function header in the definition of the function.

The prototypes for the functions used in a program must appear before the statements calling the functions, and are usually placed at the beginning of a program. Header files appear between < and > in a #include statement. The ones we've been including for standard library functions include the prototypes of the functions provided by the library.

For our power() example, we could write the prototype as follows:

double power(double value, int index);

Don't forget that a semicolon is required at the end of a function prototype. Without it, you will get error messages from the compiler.

Note that we have specified different names for the parameters in the function prototype to those we used in the function header when we defined the function. This is just to indicate that it's possible. Most often, the same names are used in the prototype and in the function header in the definition of the function, but this doesn't have to be so. The parameter names in the function prototype can be selected to aid understanding of the significance of the parameters.

If you like, you can even omit the names altogether in the prototype, and just write the following:

double power(double, int);

This is enough for the compiler to do its job. However, it's better practice to use some meaningful name in a prototype, since it aids readability and, in some cases, makes all the difference between clear and confusing code. If you have a function with two parameters of the same type (suppose our index was also of type double in the function power(), for example), the use of suitable names can indicate which parameter appears first and which second.

Try It Out - Using a Function

We can see how all this goes together in an example using our power() function:

// Ex4_01.cpp
// Declaring, defining and using a function
#include <iostream>
using namespace std;
double power(double x, int n); // Function prototype

int main(void)
{
   int index = 3;              // Raise to this power
   double x = 3.0;             // Different x from that in function power
   double y = 0.0;
   y = power(5.0, 3);          // Passing constants as arguments

   cout << endl
        << "5.0 cubed = " << y;
   cout << endl
        << "3.0 cubed = "
        << power(3.0, index);  // Outputting return value

   // Using a function as an argument
   x = power(x, power(2.0, 2.0));

   cout << endl         //with auto conversion of 2nd parameter
        << "x = " << x;

   cout << endl;
   return 0;
}

// Function to compute integral powers of a double value
// First argument is value, second argument is power index
double power(double x, int n)
{                            // Function body starts here...
   double result = 1.0;      // Result stored here
   for(int i = 1; i<=n; i++)
      result *= x;
   return result;
}                            // ...and ends here

This shows some of the ways in which we can use the function power(), specifying the arguments in a variety of ways. If you run this example, you'll get the following output:

How It Works

After the usual #include statement for input/output and the using directive, there is the prototype for the function power(). If you tried deleting this and recompiling the program, the compiler wouldn't be able to process the calls to the function in main().

In a change to previous examples, we've used the new keyword void in the function main() where the parameter list would usually appear to indicate that no parameters are to be supplied. Previously, we left the parentheses enclosing the parameter list empty, which is also interpreted in C++ as indicating that there are no parameters.

As we saw, the keyword void can also be used as the return type for a function to indicate that no value is returned. If you specify the return type of a function as void, you must not place a value in any return statement within the function - otherwise you'll get an error message from the compiler.

You'll have gathered from some of our previous examples that using a function is very simple. To use the function power() to calculate 5.03 and store the result in a variable y in our example, we've written this:

y = power(5.0, 3);

To recap, the values 5.0 and 3 here are called arguments. They happen to be constants, but any expression can be used as an argument, as long as a value of the correct type is ultimately produced. The arguments substitute for the parameters x and n, which were used in the definition of the function. The computation is performed using these values, then a copy of the result, 125, will be returned to the calling function, main(), which will then be stored in y. You can think of the function as having this value in the statement or expression in which it appears. We then output the value of y to the screen:

cout << endl
        << "5.0 cubed = " << y;

The next call of the function is used within the output statement,

cout << endl
        << "3.0 cubed = "
        << power(3.0, index); // Outputting return value

so the value returned is transferred directly to the output stream. Since we haven't stored the returned value anywhere, it is otherwise unavailable to us. The first argument in the call of the function here is a constant, while the second argument is a variable.

The function power() is used next in this statement:

// Using a function as an argument
   x = power(x, power(2.0, 2.0));

Here the function will be called twice. The first call to the function will be that which appears as an argument to the first call. Although the arguments are both specified as 2.0, the function will actually be called with the first argument as 2.0 and the second argument as 2. The compiler will convert the double value specified for the second argument to int, because it knows from the function prototype (shown again below) that the type of the second parameter has been specified as int.

double power(double x, int n);     // Function prototype

There's a possible loss of data in converting from a double to an int, something the compiler has instituted. This is a dangerous programming practice, and it is not at all obvious from the code that this conversion is intended. As we saw in the first chapter, it is far better to be explicit in your code, and to pass the argument as an int in the first place, if that is the type that the function requires.

The double result 4.0 will be returned, and after converting this to int, the compiler will insert this value as the second argument in the next call of the function, with x as the first argument. Since x has the value 3.0, the value of 3.04 will be computed and the result, 81.0, stored in x. This sequence of events is illustrated below:

Again, this involves an implicit conversion by the compiler from a double to an int. We could write this code using a static cast to make the fact that this conversion is intended absolutely clear:

x = power(x, static_cast<int>(power(2.0, static_cast<int>(2.0))));


(Passing Arguments to a Function)

Passing Arguments to a Function

It's very important to understand how arguments are passed to a function, as it will affect how you write functions and how they will ultimately operate. There are also a number of pitfalls to be avoided, so we'll look at the mechanism quite closely.

The arguments specified when a function is called should usually correspond in type and sequence to the parameters appearing in the definition of the function. As we saw in the last example, if the type of an argument specified in a function call doesn't correspond with the type of parameter in the function definition, then (where possible) it will be converted to the required type. If this proves not to be possible, you will get an error message from the compiler. However, even if the conversion is possible and the code compiles, it could well result in the loss of data (for example from type long to short) and should therefore be avoided.

One mechanism, used generally in C++ to pass parameters to functions, applies when the parameters are specified in the function definition, or prototype, as ordinary variables (not references). This is called the pass-by-value method of transferring data to a function.

The Pass-by-value Mechanism

With this mechanism, the variables or constants that you specify as arguments are not passed to a function at all. Instead, copies of the arguments are created and these copies are used as the values to be transferred. We can show this in a diagram using the example of our function power():

Each time you call the function power(), the compiler arranges for copies of the arguments that you specify to be stored in a temporary location in memory. During execution of the functions, all references to the function parameters will be mapped to these temporary copies of the arguments.

Purely to help your understanding of the diagram, we have used pseudo-names for the copies generated in the illustration. In reality, they do not exist in this form.

Try It Out - Passing-by-value

One consequence of the pass-by-value mechanism is that a function can't directly modify the arguments passed. We can demonstrate this by deliberately trying to do so in an example:

// Ex4_02.cpp
// A futile attempt to modify caller arguments
#include <iostream>
using namespace std;
int incr10(int num);          // Function prototype

int main(void)
{
   int num = 3;
   cout << endl
        << "incr10(num) = " << incr10(num)
        << endl
        << "num = " << num;
   cout << endl;
   return 0;
}

// Function to increment a variable by 10
int incr10(int num)     // Using the same name might help...
{
   num += 10;           // Increment the caller argument - hopefully
   return num;          // Return the incremented value
}

How It Works

Of course, this program is doomed to failure. If you run it, you will get this output:

This confirms that the original value of num remains untouched. The incrementing occurred on the copy of num that was generated, and was eventually discarded on exiting from the function.

Clearly, the pass-by-value mechanism provides you with a high degree of protection from having your caller arguments mauled by a rogue function, but it is conceivable that we might actually want to arrange to modify caller arguments. Of course, there is a way to do this. Didn't you just know that pointers would turn out to be incredibly useful?

Pointers as Arguments to a Function

When you use a pointer as an argument, the pass-by-value mechanism still operates as before. However, a pointer is an address of another variable, and if you take a copy of this address, the copy still points to the same variable. This is how specifying a pointer as a parameter enables your function to get at a caller argument.

Try It Out - Pass-by-pointer

We can change the last example to use a pointer to demonstrate the effect:

// Ex4_03.cpp
// A successful attempt to modify caller arguments
#include <iostream>
using namespace std;
int incr10(int* num);      // Function prototype

int main(void)
{
   int num = 3;
   int* pnum = &num;       // Pointer to num
   cout << endl
        << "Address passed = " << pnum;
   cout << endl
        << "incr10(pnum) = " << incr10(pnum);
   cout << endl
        << "num = " << num;
   cout << endl;
   return 0;
}

// Function to increment a variable by 10
int incr10(int* num)    // Function with pointer argument
{
   cout << endl
        << "Address received = " << num;
   *num += 10;    // Increment the caller argument - confidently
   return *num;   // Return the incremented value
}

How It Works

In this example, the principal alterations from the previous version relate to passing a pointer, pnum, in place of the original variable, num. The prototype for the function now has the parameter type specified as a pointer to int, and the main() function has the pointer pnum declared and initialized with the address of num. The function main(), and the function incr10(), output the address sent and the address received respectively, to verify that the same address is indeed being used in both places.

If you run this program, you'll get output similar to this:

The address values produced by your computer may well be different from those shown above, but the two values should be identical to each other.

The output shows that this time the variable num has been incremented and has a value that's now identical to that returned by the function.

In the rewritten version of the function incr10(), both the statement incrementing the value passed to the function, and the return statement, now need to de-reference the pointer in order to use the value stored.

Passing Arrays to a Function

You can also pass an array to a function, but in this case the array is not copied, even though a pass-by-value method of passing arguments still applies. The array name is converted to a pointer, and a copy of the pointer to the beginning of the array is passed to the function. This is quite advantageous, as copying large arrays could be very time consuming.

Try It Out - Passing Arrays

We can illustrate the ins and outs of this by writing a function to compute the average of a number of values that are passed to a function in an array.

// Ex4_04.cpp
// Passing an array to a function
#include <iostream>
using namespace std;

//Function prototype
double average(double array[], int count);

int main(void)
{
   double values[] = { 1.0, 2.0, 3.0, 4.0,
                            5.0, 6.0, 7.0, 8.0, 9.0, 10.0 };
   cout << endl
        << "Average = "
        << average(values, (sizeof values)/(sizeof values[0]));
   cout << endl;
   return 0;
}

// Function to compute an average
double average(double array[], int count)
{
   double sum = 0.0;                 // Accumulate total in here
   for(int i = 0; i<count; i++)
      sum += array[i];               // Sum array elements
   return sum/count;                 // Return average
}

How It Works

The function average() is designed to work with an array of any length. As you can see from the prototype, it accepts two arguments: the array and a count of the number of elements. Since we want it to work with arrays of arbitrary length, the array parameter appears without a specified dimension.

The function is called in main() in this statement,

cout << endl
     << "Average = "
     << average(values, (sizeof values)/(sizeof values[0]));

with the first argument as the array name, values, and the second argument as an expression which evaluates to the number of elements in the array.

You'll recall this expression, using the operator sizeof, from when we looked at arrays in Chapter 3.

Within the body of the function, the computation is expressed in the way you would expect. There's no significant difference between this and the way we would write the same computation if we implemented it directly in main().

If you run the example, it will produce the following output:

This confirms that everything works as we anticipated.

Try It Out - Using Pointer Notation When Passing Arrays

However, we haven't exhausted all the possibilities here. As we determined at the outset, the array name is passed as a pointer-in fact, as a copy of a pointer, so within the function we needn't necessarily deal with the data as an array at all. We could modify the function in the example to work with pointer notation throughout, in spite of the fact that we are using an array.

// Ex4_05.cpp
// Handling an array in a function as a pointer
#include <iostream>
using namespace std;

double average(double* array, int count);  //Function prototype

int main(void)
{
   double values[] = { 1.0, 2.0, 3.0, 4.0, 5.0,
                       6.0, 7.0, 8.0, 9.0, 10.0 };
   cout << endl
        << "Average = "
        << average(values, (sizeof values)/(sizeof values[0]));
   cout << endl;
   return 0;
}

// Function to compute an average
double average(double* array, int count)
{
   double sum = 0.0;                 // Accumulate total in here
   for(int i = 0; i<count; i++)
      sum += *array++;               // Sum array elements
   return sum/count;                 // Return average
}

How It Works

As you can see, the program needed very few changes to make it work with the array as a pointer. The prototype and the function header have been changed, although neither change is absolutely necessary. If you change both back to the original version with the first parameter specified as a double array, and leave the function body written in terms of a pointer, it will work just as well. The most interesting aspect of this version is the for loop statement:

sum += *array++;               // Sum array elements

Here we apparently break the rule about not being able to modify an address specified as an array name, because we are incrementing the address stored in array. In fact, we aren't breaking the rule at all. Remember that the pass-by-value mechanism makes a copy of the original array address and passes that, so we are just modifying the copy here - the original array address will be quite unaffected. As a result, whenever we pass a one-dimensional array to a function, we are free to treat the value passed as a pointer in every sense, and change the address in any way we wish.

Naturally, this version produces exactly the same output as the original.

Passing Multi-Dimensional Arrays to a Function

Passing a multi-dimensional array to a function is quite straightforward. The following line declares a two dimensional array, beans:

double beans[2][4];

You could then write the prototype of a hypothetical function, yield(), like this:

double yield(double beans[2][4]);

You may be wondering how the compiler can know that this statement is defining an array of the dimensions shown as an argument, and not a single array element. The answer is simple - you can't write a single array element as a parameter in a function definition or prototype, although you can pass one as an argument when you call a function. For a parameter accepting a single element of an array as an argument, the parameter would just have a variable name. The array context wouldn't apply.

When you are defining a multi-dimensional array as a parameter, you can also omit the first dimension value. Of course, the function will need some way of knowing the extent of the first dimension. For example, you could write this:

double yield(double beans[][4], int index);

Here, the second parameter would provide the necessary information about the first dimension. The function can operate with a two-dimensional array with any value for the first dimension, but with the second dimension fixed at 4.

Try It Out - Passing Multi-Dimensional Arrays

We define such a function in the following example:

// Ex4_06.cpp
// Passing a two-dimensional array to a function
#include <iostream>
using namespace std;

double yield(double array[][4], int n);

int main(void)
{
   double beans[3][4] =  {   { 1.0,  2.0,  3.0,  4.0 },
                             { 5.0,  6.0,  7.0,  8.0 },
                             { 9.0, 10.0, 11.0, 12.0 }   };
   cout << endl
        << "Yield = "
        << yield(beans, sizeof beans/sizeof beans[0]);
   cout << endl;
   return 0;
}

// Function to compute total yield
double yield(double beans[][4], int count)
{
   double sum = 0.0;
   for(int i=0; i<count; i++) // Loop through number of rows
      for(int j=0; j<4; j++)  // Loop through elements in a row
         sum += beans[i][j];
   return sum;
}

How It Works

We have used different names for the parameters in the function header from those in the prototype, just to remind you that this is possible - but in this case, it doesn't really improve the program at all. The first parameter is defined as an array of an arbitrary number of rows, each row having four elements. We actually call the function using the array beans with three rows. The second argument is specified by dividing the total size of the array in bytes by the size of the first row. This will evaluate to the number of rows in the array.

The computation in the function is simply a nested for loop with the inner loop summing elements of a single row and the outer loop repeating this for each row. For what it's worth, the program will display this result:

Using a pointer in a function rather than a multi-dimensional array as an argument doesn't really apply particularly well. When the array is passed, it passes an address value which points to an array of four elements (a row) 4. This doesn't lend itself to an easy pointer operation within the function. We would need to modify the statement in the nested for loop to the following,

sum += *(*(beans+i)+j);

so the computation is almost certainly clearer in array notation.

References as Arguments to a Function

Specifying a parameter to a function as a reference changes the method of passing data for that parameter. The method used is not pass-by-value, where an argument is copied before being passed, but pass-by-reference where the parameter acts as an alias for the argument passed. This eliminates any copying and allows the function to access the caller argument directly. It also means that the de-referencing, which is required when passing and using a pointer to a value, is also unnecessary.

Try It Out - Pass-by-reference

Let's go back to a revised version of a very simple example, Ex4_03.cpp, to see how it would work using reference parameters:

// Ex4_07.cpp
// Using a reference to modify caller arguments
#include <iostream>
using namespace std;

int incr10(int& num);             // Function prototype

int main(void)
{
   int num = 3;
   int value = 6;
   cout << endl
        << "incr10(num) = " << incr10(num);
   cout << endl
        << "num = " << num;
   cout << endl
        << "incr10(value) = " << incr10(value);
   cout << endl
        << "value = " << value;
   cout << endl;
   return 0;
}

// Function to increment a variable by 10
int incr10(int& num) // Function with reference argument
{
   cout << endl
        << "Value received = " << num;
   num += 10;    // Increment the caller argument - confidently
   return num;   // Return the incremented value
}

How It Works

You should find the way this works quite remarkable. This is essentially the same as Ex4_03.cpp, except that the function uses a reference as a parameter. The prototype has been changed to reflect this. When the function is called, the argument is specified just as though it is a pass-by-value operation, that is, it's used in the same way as the earlier version. The argument value isn't passed to the function. Here, the function parameter is initialized with the address of the argument, so whenever the parameter num is used in the function, it accesses the caller argument directly.

Just to reassure you that there's nothing fishy about the use of the identifier num in main() as well as in the function, the function is called a second time with the variable value as the argument. At first sight this may give you the impression that it contradicts what we said was a basic property of a reference - that once declared and initialized, it couldn't be reassigned to another variable. The reason it isn't contradictory is that a reference as a function parameter is created and initialized when the function is called, and destroyed when the function ends, so we get a completely new reference each time we use the function.

Within the function, the value received from the calling program is displayed on the screen. Although the statement is essentially the same as the one used to output the address stored in a pointer, because num is now a reference, we obtain the data value rather than the address.

This clearly demonstrates the difference between a reference and a pointer. A reference is an alias for another variable, and therefore can be used as an alternative way of referring to it. It is equivalent to using the original variable name.

The output from this example is as follows:

This shows that the function incr10() is directly modifying the variable passed as a caller argument.

You will find that if you try to use a numeric value, such as 20, as an argument to incr10(), the compiler will output an error message. This is because the compiler recognizes that a reference parameter can be modified within a function, and the last thing you want is to have your constants changing value now and again. This would introduce a kind of excitement into your programs that you could probably do without.

This security is all very well, but if the function didn't modify the value, we wouldn't want the compiler to create all these error messages every time we pass a reference argument that was a constant. Surely there ought to be some way to accommodate this? As Ollie would have said, 'There most certainly is, Stanley!'

Use of the const Modifier

We can use the const modifier with a parameter to a function to tell the compiler that we don't intend to modify it in any way. This will cause the compiler to check that your code indeed does not modify the argument, and there will be no error messages when you use a constant argument.

Try It Out - Passing a const

We can modify the previous program to show how the const modifier changes the situation.

// Ex4_08.cpp
// Using a reference to modify caller arguments
#include <iostream>
using namespace std;

int incr10(const int& num);             // Function prototype

int main(void)
{
   // Declared const to test for temporary creation
   const int num = 3;
   int value = 6;
   cout << endl
        << "incr10(num) = " << incr10(num);
   cout << endl
        << "num = " << num;
   cout << endl
        << "incr10(value) = " << incr10(value);
   cout << endl
        << "value = " << value;
   cout << endl;
   return 0;
}

// Function to increment a variable by 10
// Function with const reference argument
int incr10(const int& num)
{
   cout << endl
        << "Value received = " << num;
//   num += 10;     // this statement would now be illegal
   return num+10;   // Return the incremented value
}

How It Works

We declare the variable num in main() as const to show that when the parameter to the function incr10() is declared as const, we no longer get a compiler message when passing a const object.

It has also been necessary to comment out the statement which increments num in the function incr10(). If you uncomment this line, you'll find the program will no longer compile, because the compiler won't allow num to appear on the left-hand side of an assignment. When you specified num as const in the function header and prototype, you promised not to modify it, and so the compiler checks that you kept your word.

Everything works as before, except that the variables in main() are no longer changed in the function, so the program produces the following output:

Now, by using reference arguments, we have the best of both worlds. On one hand, we can write a function that can access caller arguments directly, and avoid the copying that is implicit in the pass-by-value mechanism. On the other hand, where we don't intend to modify an argument, we can get all the protection against accidental modification we need by using a const modifier with a reference.


(Returning Values from a Function)

Returning Values from a Function

All the example functions that we have created have returned a single value. Is it possible to return anything other than a single value? Well, not directly, but as we said earlier, the single value returned needn't be a numeric value; it could also be an address, which provides the key to returning any amount of data. You simply use a pointer. Unfortunately, the pitfalls start here, so you need to keep your wits about you for the adventure ahead.

Returning a Pointer

Returning a pointer value is very easy. A pointer value is just an address, so if you want to return the address of some variable value, you can just write the following:

return &value;          // Returning an address

As long as the function header and function prototype indicate the return type appropriately, we have no problem - or at least no apparent problem. Assuming that the variable value is of type double, the prototype of a function called treble, which might contain the above return statement, could be as follows,

double* treble(double data);

with some arbitrary parameter list.

So let's look at a function that will return a pointer. It's only fair that I warn you in advance - this function doesn't work. Let's assume that we need a function which will return a pointer to three times its argument value. Our first attempt might look like this:

// Function to treble a value - mark 1
double* treble(double data)
{
   double result = 0;
   result = 3.0*data;
   return &result;
}

Try It Out - Returning a Bad Pointer

We could create a little test program to see what happens (remember that the treble function won't work as expected):

//Ex4_09.cpp
#include <iostream>
using namespace std;

double* treble(double);           // Function prototype

int main(void)
{
   double num = 5.0;              // Test value
   double* ptr = 0;               // Pointer to returned value
   ptr = treble(num);
   cout << endl
        << "Three times num = " << 3.0*num;
   cout << endl
        << "Result = " << *ptr;   // Display 3*num
   cout << endl;
   return 0;
}

// Function to treble a value - mark 1
double* treble(double data)
{
   double result = 0;
   result = 3.0*data;
   return &result;
}

How It Works (or Why It Doesn't)

The function main() calls the function treble() and stores the address returned in the pointer ptr, which should point to a value which is three times the argument, num. We then display the result of computing three times num, followed by the value at the address returned from the function.

On my computer, I get this output:

Clearly, the second line doesn't reflect the correct value of 15, but where's the error? Well, it's not exactly a secret, since the compiler gives fair warning of the problem with the message Warning C4172: returning address of local variable or temporary.

The error arises because the variable result in the function treble() is created when the function begins execution, and is destroyed on exiting from the function - so the memory that the pointer is pointing to no longer contains the original variable value. The memory previously allocated to result becomes available for other purposes, and here it has evidently been used for something else.

A Cast Iron Rule for Returning Addresses

There is an absolutely cast iron rule for returning addresses:

Never return the address of a local automatic variable from a function.

Now that we have a function that doesn't work, we need to think about what to do to rectify that. We could use a reference and modify the original variable, but that's not what we set out to do. We are trying to return a pointer to some useful data so that, ultimately, we can return more than a single item of data. One answer lies in dynamic memory allocation (we saw this in action in the last chapter). With the operator new, we can create a new variable in the free store, which will continue to exist until it is eventually destroyed by delete - or until the program ends. The function would then look like this:

// Function to treble a value - mark 2
double* treble(double data)
{
   double* result = new double(0.0);
   if(!result)
   {
      cout << "Memory allocation failed.";
      exit(1);
   }
   *result = 3.0*data;
   return result;
}

Rather than declaring result as of type double, we now declare it as double* and store in it the address returned by the operator new. We then have the necessary check that we received a valid address, and exit the program if anything is wrong.

Since the result is a pointer, the rest of the function is changed to reflect this, and the address contained in the result is finally returned to the calling program. You could test this by replacing the function in the last working example with this version.

You need to remember that, with dynamic memory allocation from within a function like this, more memory is allocated each time the function is called. The onus is on the calling program to delete the memory when it's no longer required. It's easy to forget to do this in practice, with the result that the free store is gradually eaten up until, at some point, it is exhausted and the program will fail. This sort of problem is often referred to as a memory leak.

Here you can see how the function would be used. The two changes that we needed to make to the original code, once the function has been replaced, are to include the standard library header file and to use delete to free the memory as soon as we finish using the returned pointer.

#include <iostream>
#include <cstdlib>
using namespace std;

double* treble(double);         // Function prototype

int main(void)
{
   double num = 5.0;            // Test value
   double* ptr=0;               // Pointer to returned value
   ptr = treble(num);
   cout << endl
        << "Three times num = " << 3.0*num;
   cout << endl
        << "Result = " << *ptr; // Display 3*num
   delete ptr;              // Don't forget to free the memory
   cout << endl;
   return 0;
}

// Function to treble a value - mark 2
double* treble(double data)
{
   double* result = new double(0.0);
   if(!result)
   {
      cout << "Memory allocation failed.";
      exit(1);
   }
   *result = 3.0*data;
   return result;
}

Returning a Reference

You can also return a reference from a function. This is just as fraught with potential errors as returning a pointer, so you need to take care with this too. Because a reference has no existence in its own right (it's always an alias for something else), you must ensure that the object it refers to still exists after the function completes execution. It's very easy to forget this when you use references in a function, because they appear to be just like ordinary variables.

References as return types are of primary significance in the context of object-oriented programming. As you will see later in the book, they will enable you to do things that would otherwise be impossible. (This particularly applies to 'operator overloading'.) The principal characteristic of a reference-type return value is that it is an lvalue. This means that you can use the result of a function on the left-hand side of an assignment statement.

Try It Out - Returning a Reference

Let's look at one example which illustrates the use of reference return types, and also demonstrates how a function can be used on the left of an assignment operation when it returns an lvalue. We will assume that we have an array containing a mixed set of values. Whenever we want to insert a new value into the array, we want to replace the lowest value.

// Ex4_10.cpp
// Returning a reference
#include <iostream>
#include <iomanip>
using namespace std;

double& lowest(double A[], int len);  // Prototype of function
                                      // returning a reference

int main(void)
{
   double array[] = { 3.0, 10.0, 1.5, 15.0, 2.7, 23.0,
                      4.5, 12.0, 6.8, 13.5, 2.1, 14.0 };
   // Initialize to number of elements
   int len = sizeof array/sizeof array[0];
   cout << endl;
   for(int i = 0; i<len; i++)
      cout << setw(6) << array[i];
   lowest(array, len) = 6.9;     // Change lowest to 6.9
   lowest(array, len) = 7.9;     // Change lowest to 7.9
   cout << endl;
   for(i = 0; i<len; i++)
      cout << setw(6) << array[i];
   cout << endl;
   return 0;
}

double& lowest(double A[], int len)
{
   int j=0;               // Index of lowest element
   for(int i=1; i<len; i++)
      if(A[j]>A[i])       // Test for a lower value...
         j = i;           // ...if so update j
   return A[j];           // Return reference to lowest element
}

How It Works

Let's first take a look at how the function is implemented. The prototype for the function lowest() uses double& as the specification of the return type, which is therefore of type 'reference to double'. You write a reference-type return value in exactly the same way as we've already seen for variable declarations, appending the & to the data type. The function has two parameters specified: a one-dimensional array A of type double, and an int parameter len, which should specify the length of the array.

The body of the function has a straightforward for loop to determine which element of the array passed to it contains the lowest value. The index, j, of the array element with the lowest value is arbitrarily set to 0 at the outset, and then modified within the loop if the current element, A[i], is less than A[j]. Thus, on exit from the loop, j will contain the index value corresponding to the array element with the lowest value. The return statement is as follows:

return A[j];       // Return reference to lowest element

In spite of the fact that this looks identical to the statement which would return a value, because the return type was declared as a reference, this returns a reference to A[j] rather than the value that the element contains. The address of A[j] is used to initialize the reference to be returned. This reference is created by the compiler, because the return type was declared as a reference.

Don't confuse returning &A[j] with returning a reference. If you write &A[j] as the return value, you are specifying the address of A[j], which is a pointer. If you do this after having specified the return type as a reference, you will get an error message from the compiler.

The function main(), which tests our function lowest(), is very simple. An array of type double is declared and initialized with 12 arbitrary values, and an int variable len is initialized to the length of the array. The initial values in the array are output for comparison purposes.

Again, we've used the stream manipulator setw() to space the values uniformly, requiring the #include statement for iomanip

The function main() then calls the function lowest() on the left side of an assignment to change the lowest value in the array. This is done twice to show that it does actually work and is not an accident. The contents of the array are then output to the display again, with the same field width as before, so corresponding values line up. If you run this example, you should see the following output:

As you can see, with the first call to lowest(), the third element of the array, array[2], contained the lowest value, so the function returned a reference to it and its value was changed to 6.9. Similarly, on the second call, array[10] was changed to 7.9.

This demonstrates quite clearly that returning a reference allows the use of the function on the left side of an assignment statement. The effect is as if the variable specified in the return statement appeared on the left of the assignment.

Of course, if you want to, you can also use it on the right side of an assignment, or in any other suitable expression. If we had two arrays, X and Y, with lenX and lenY elements respectively, we could set the lowest element in the array X to twice the lowest element in the array Y with this statement:

lowest(X, lenX) = 2.0*lowest(Y, lenY);

This statement would call our function lowest() twice, once with arguments Y and lenY in the expression on the right side of the assignment, and once with arguments X and lenX to obtain the address where the result of the right-hand expression is to be stored.

A Teflon-Coated Rule: Returning References

A similar rule to the one concerning the return of a pointer from a function also applies to returning references:

Never return a reference to a local variable from a function.

We'll leave the topic of returning a reference from a function for now, but we haven't finished with it yet. We'll come back to it again in the context of user-defined types and object-oriented programming, when we shall unearth a few more magical things that we can do with references.

Static Variables in a Function

There are some things you can't do with automatic variables within a function. You can't count how many times a function is called, for example, because you can't accumulate a value from one call to the next. There's more than one way to get around this if you need to. For instance, you could use a reference parameter to update a count in the calling program, but this wouldn't help if the function was called from lots of different places within a program. You could use a global variable which you incremented from within the function, but globals are risky things to use as they can be accessed from anywhere in a program, which makes it very easy to change them accidentally.

For a general solution, you can declare a variable within a function as static. You use exactly the same form of declaration for a static variable that we saw in Chapter 1. For example, to declare a variable count as static you could use this statement:

static int count = 0;

This also initializes the variable to zero.

Initialization of a static variable within a function only occurs the first time that the function is called. In fact, on the first call of a function, the static variable is created and initialized. It then continues to exist for the duration of program execution, and whatever value it contains when the function is exited is available when the function is next called.

Try It Out - Using Static Variables in Functions

We can demonstrate how a static variable behaves in a function with the following simple example:

// Ex4_11.cpp
// Using a static variable within a function
#include <iostream>
using namespace std;

// Function prototype, no arguments or return value
void record(void);

int main(void)
{
   record();
   for(int i = 0; i<= 3; i++)
      record();
   cout << endl;
   return 0;
}

// A function that records how often it is called
void record(void)
{
   static int count = 0;
   cout << endl
        << "This is the " << ++count;
   if((count>3) && (count<21))  // All this....
      cout <<"th";
   else
      switch(count%10)          // is just to get...
      {
         case 1: cout << "st";
                 break;
         case 2: cout << "nd";
                 break;
         case 3: cout << "rd";
                 break;
         default: cout << "th"; // the right ending for...
      }                         // 1st, 2nd, 3rd, 4th, etc.
   cout << " time I have been called";
   return;
}

Our function here serves only to record the fact that it was called. If you build and execute it you will get this output:

How It Works

The static variable count is initialized with 0, and is incremented in the first output statement in the function. Because the increment operation is prefix, the incremented value is displayed by the output statement. It will be 1 on the first call, 2 on the second and so on. Because the variable count is static, it continues to exist and retain its value from one call of the function to the next.

The remainder of the function is concerned with working out when 'st', 'nd', 'rd' or 'th' should be appended to the value of count that is displayed. It's surprisingly irregular. (I guess 101 should be 101st rather than 101th, shouldn't it?)

Note the return statement. Because the return type of the function is void, to include a value would cause a compiler error. You don't actually need to put a return statement in this particular case as running off the closing brace for the body of the function is equivalent to the return statement without a value. The program will compile and run without error either way.


(Recursive Function Calls)

Recursive Function Calls

When a function contains a call to itself, it's referred to as a recursive function. A recursive function call can also be indirect, where a function fun1 calls a function fun2, which in turn calls fun1.

Recursion may seem like a recipe for an infinite loop, and if you aren't careful it certainly can be. An infinite loop will lock up your machine and require Ctrl-Alt-Del, which is always a nuisance. A prerequisite for avoiding an infinite loop is that the function contains some means of stopping the process.

Unless you have come across the technique before, the sort of things to which recursion may be applied may not be obvious. In physics and mathematics there are many things which can be thought of as involving recursion. A simple example is the factorial of an integer which for a given integer N, is the product 1x2x3...xN. This is very often the example given to show recursion in operation. However, we shall look at something even simpler.

Try It Out - A Recursive Function

At the start of the chapter (see Ex4_01.cpp) we produced a function to compute the integral power of a value, that is to compute xn. This is equivalent to x multiplied by itself n times. We'll implement this as a recursive function as an elementary illustration of recursion in action.

// Ex4_12.cpp (based on Ex4_01.cpp)
// A recursive version of x to the power n
#include <iostream>
#include <cstdlib>             // This is for the exit() function
using namespace std;

double power(double x, int n); // Function prototype

int main(void)
{
   int index = 3;     // Raise to this power
   double x = 3.0;    // Different x from that in function power
   double y = 0.0;
   y = power(5.0, 3); // Passing constants as arguments

   cout << endl
        << "5.0 cubed = " << y;

   cout << endl
        << "3.0 cubed = "
        << power(3.0, index);     // Outputting return value

   // Using a function as an argument
   x = power(x, static_cast<int>(power(2.0, 2)));

   // with auto conversion of 2nd parameter
   cout << endl
        << "x = " << x;

   cout << endl;
   return 0;
}

// Recursive function to compute integral powers of a double
// value - first argument is a double value, and the second
// argument is a power index.
double power(double x, int n)
{
   if(n<0)
   {
      cout << endl
           << "Negative index, program terminated.";
      exit(1);
   }
   if(n)
      return x*power(x, n-1);
   else
      return 1.0;
}

The function main() is exactly the same as the previous version so the output is also the same:

We have added the #include statement for cstdlib, because we use the exit() function in our revised function power(). Let's now look at how the function works.

How It Works

We only intend to support positive powers of x, so the first action is to check that the value for the power that x is to be raised to n, is not negative. This is essential for our implementation of a recursive function, otherwise we could get an infinite loop with negative values for n because of the way the function is written. The if statement provides for the value 1.0 being returned if n is zero, and in all other cases it returns the result of the expression, x*power(x, n-1). This causes a further call to the function power() with the index value reduced by 1.

Clearly, within the function power(), if the value of n is greater than zero, a further call to the function power() will occur. In fact, for any given value of n greater than 0, the function will call itself n times. The mechanism is illustrated in the following figure, assuming the value 3 for the index argument.

As you see, we need a total of four calls to the power() function to generate x3.

Using Recursion

Unless you have a problem which particularly lends itself to using recursive functions, or if you have no obvious alternative, it's generally better to use a different approach, such as a loop. This will be much more efficient than using recursive function calls. Think about what happens with our last example to evaluate a simple product, x*x*...x, n times. On each call, the compiler will generate copies of the two arguments to the function, and also has to keep track of the location to return to when each return is executed. It's also necessary to arrange to save the contents of various registers in your computer so that they can be used within the function power(), and of course these will need to be restored to their original state at each return from the function. With a quite modest depth of recursive call, the overhead can be considerably greater than if you use a loop.

This is not to say you should never use recursion. Where the problem suggests the use of recursive function calls as a solution, it can be an immensely powerful technique, greatly simplifying the code. We'll see an example where this is the case in the next chapter.


(Summary)

Summary

In this chapter, you've learned about the basics of program structure. You should have a good grasp of how functions are defined, how data can be passed to a function and how results are returned to a calling program. Functions are fundamental to programming in C++, so everything we do from here on will involve using multiple functions in a program. The key points that you should keep in mind about writing your own functions are these:

  • Functions should be compact units of code with a well-defined purpose. A typical program will consist of a large number of small functions, rather than a small number of large functions.

  • Always provide a function prototype for each function defined in your program, positioned before you call that function.

  • Passing values to a function using a reference can avoid the copying implicit in the call-by-value transfer of arguments. Parameters that are not modified in a function should be specified as const.

  • When returning a reference or a pointer from a function, ensure that the object being returned has the correct scope. Never return a pointer or a reference to an object which is local to a function.

The use of references as arguments is a very important concept, so make sure you are confident about using them. We'll see a lot more about references as arguments to functions when we look into object-oriented programming.


(More About Program Structure)

Chapter 5 - More About Program Structure

In the previous chapter, you learned about the basics of defining functions and the various ways in which data can be passed to a function. You also saw how results are returned to a calling program.

In this chapter, we will cover the further aspects of how functions can be put to good use, including:

  • What a pointer to a function is

  • How to define and use pointers to functions

  • How to write a function to manage dynamic memory allocation errors

  • How to define and use arrays of pointers to functions

  • How to write multiple functions with a single name to handle different kinds of data automatically

  • What function templates are and how you define and use them

  • How multi-file programs are structured and how to refer to a variable defined in another file

  • How to declare and use your own namespaces


(Pointers to Functions)

Pointers to Functions

A pointer stores an address value which, up to now, has been the address of another variable with the same basic type as the pointer. This has provided considerable flexibility in allowing us to use different variables at different times through a single pointer. A pointer can also point to the address of a function. This enables you to call a function through a pointer, which will be the function at the address that was last assigned to the pointer.

Obviously, a pointer to a function must contain the memory address of the function that you want to call. To work properly, however, the pointer must also maintain information about the parameter list for the function it points to, as well as the return type. Therefore, when you declare a pointer to a function, you have to specify the parameter types and the return type of the functions that it can point to, in addition to the name of the pointer. Clearly, this is going to restrict what you can store in a particular pointer to a function. If you have declared a pointer to functions that accept one argument of type int and return a value of type double, then you can only store the address of a function that has exactly the same form. If you want to store the address of a function that accepts two arguments of type int and returns type char, then you must define another pointer with these characteristics.

Declaring Pointers to Functions

Let's declare a pointer pfun that we can use to point to functions that take two arguments of type char* and int, and return a value of type double. The declaration would be as follows:

double (*pfun)(char*, int); // Pointer to function declaration

The parentheses may make this look a little weird at first. This declares a pointer, pfun, which can point to functions which accept two arguments of type pointer to char and of type int, and which return a value of type double. The parentheses around the pointer name, pfun, and the asterisk, are necessary: without them, it would be a function declaration rather than a pointer declaration. In this case, it would look like this:

double *pfun(char*, int); // Prototype for a function
                          // returning type double*

This is a prototype for a function pfun() which has two parameters, and returns a pointer to a double value. Since we were looking to declare a pointer, this is clearly not what we want at the moment.

The general form of a declaration of a pointer to a function is given here:

return_type (*pointer_name)(list_of_parameter_types);

The pointer can only point to functions with the same return_type and list_of_parameter_types specified in the declaration.

This shows that the declaration breaks down into three components:

  • The return type of the functions that can be pointed to

  • The pointer name preceded by an asterisk to indicate it is a pointer

  • The parameter types of the functions that can be pointed to

    If you attempt to assign a function to a pointer that does not conform to the types in the pointer declaration, the compiler generates an error message.

You can initialize a pointer to a function with the name of a function within the declaration of the pointer. This is what it might look like:

// Function prototype
long sum(long num1, long num2);
// Pointer to function points to sum()
long (*pfun)(long, long) = sum;

Here, the pointer can be set to point to any function that accepts two arguments of type long, and also returns a value of type long.

Of course, you can also initialize a pointer to a function by using an assignment statement. Assuming the pointer pfun has been declared as above, we could set the value of the pointer to a different function with these statements:

long product(long, long); // Function prototype
...
pfun = product;           // Set pointer to function product()

As with pointers to variables, you must ensure that a pointer to a function is initialized before you use it to call a function. Without initialization, catastrophic failure of your program is guaranteed.

Try It Out - Pointers to Functions

To get a proper feel for these newfangled pointers and how they perform in action, let's try one out in a program:

// Ex5_01.cpp
// Exercising pointers to functions
#include <iostream>
using namespace std;

long sum(long a, long b);            // Function prototype
long product(long a, long b);        // Function prototype

int main()
{
   // Pointer to function declaration
   long (*pdo_it)(long, long);
   pdo_it = product;
   cout << endl
        // Call product thru a pointer
        << "3*5 = " << pdo_it(3, 5);
   pdo_it = sum;                     // Reassign pointer to sum()
   cout << endl
        << "3*(4+5) + 6 = "
        // Call thru a pointer twice
        << pdo_it(product(3, pdo_it(4, 5)), 6);
   cout << endl;
   return 0;
}

// Function to multiply two values
long product(long a, long b)
{
   return a*b;
}
// Function to add two values
long sum(long a, long b)
{
   return a+b;
}

How It Works

This is hardly a useful program, but it does show very simply how a pointer to a function is declared, assigned a value and subsequently used to call a function.

After the usual preamble, we declare a pointer to a function, pdo_it, which can point to either of the other two functions that we have defined, sum() or product(). The pointer is given the address of the function product() in this assignment statement:

pdo_it = product;

When initializing an ordinary pointer, the name of the function is used in a similar way to that of an array name in that no parentheses or other adornments are required. The function name is automatically converted to an address which is stored in the pointer.

The function product() is then called indirectly through the pointer pdo_it in the output statement.

cout << endl
        // Call product thru a pointer
        << "3*5 = " << pdo_it(3, 5);

The name of the pointer is used just as if it were a function name, and is followed by the arguments between parentheses exactly as they would appear if the original function name were being used directly.

Just to show that we can do it, we change the pointer to point to the function sum(). We then use it again in a ludicrously convoluted expression to do some simple arithmetic. This shows that a pointer to a function can be used in exactly the same way as the function that it points to. The sequence of actions in the expression is shown here:

A Pointer to a Function as an Argument

Since 'pointer to a function' is a perfectly reasonable type, a function can also have a parameter that is a pointer to a function. The function can then call the function pointed to by the argument. Since the pointer can be made to point at different functions in different circumstances, this allows the particular function that is to be called from inside a function to be determined in the calling program. In this case, you can pass a function explicitly as an argument.

Try It Out - Passing a Function Pointer

We can look at this with an example. Suppose we need a function that will process an array of numbers by producing the sum of the squares of each of the numbers on some occasions, and the sum of the cubes on other occasions. One way of achieving this is by using a pointer to a function as an argument.

//Ex5_02.cpp
// A pointer to a function as an argument
#include <iostream>
using namespace std;

// Function prototypes
double squared(double);
double cubed(double);
double sumarray(double array[], int len, double (*pfun)(double));

int main()
{
   double array[] = { 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5 };
   int len = sizeof array/sizeof array[0];
   cout << endl
        << "Sum of squares = "
        << sumarray(array,len,squared);
   cout << endl
        << "Sum of cubes = "
        << sumarray(array,len,cubed);
   cout << endl;
   return 0;
}

// Function for a square of a value
double squared(double x)
{
   return x*x;
}

// Function for a cube of a value
double cubed(double x)
{
   return x*x*x;
}

// Function to sum functions of array elements
double sumarray(double array[], int len, double (*pfun)(double))
{
   double total = 0.0;     // Accumulate total in here
   for(int i=0; i<len; i++)
      total += pfun(array[i]);
   return total;
}

If you compile and run this code, you should see the following output:

How It Works

The first statement of interest is the prototype for the function sumarray(). Its third parameter is a pointer to a function which has a parameter of type double, and returns a value of type double.

double sumarray(double array[], int len, double (*pfun)(double));

The function sumarray() processes each element of the array passed as its first argument with whatever function is pointed to by its third argument. The function then returns the sum of the processed array elements.

We call the function sumarray() twice in main(), the first time with squared as the third argument, and the second time using cubed. In each case, the address corresponding to the function name used as an argument, will be substituted for the function pointer in the body of the function sumarray(), so the appropriate function will be called within the for loop.

There are obviously easier ways of achieving what this example does, but using a pointer to a function provides you with a lot of generality. You could pass any function to sumarray() that you care to define as long as it takes one double argument and returns a value of type double.

Arrays of Pointers to Functions

In the same way as regular pointers, you can declare an array of pointers to functions. You can also initialize them in the declaration. An example of declaring an array of pointers would be:

double sum(double, double);             // Function prototype
double product(double, double);         // Function prototype
double difference(double, double);      // Function prototype

// Array of function pointers
double (*pfun[3])(double,double) = { sum, product, difference };

Each of the elements in the array is initialized by the corresponding function address appearing in the initializing list between braces. To call the function product() using the second element of the pointer array, you would write:

pfun[1](2.5, 3.5);

The square brackets which select the function pointer array element appear immediately after the array name and before the arguments to the function being called. Of course, you can place a function call through an element of a function pointer array in any appropriate expression in which the original function might legitimately appear, and the index value selecting the pointer can be any expression producing a valid index value.


(Initializing Function Parameters)

Initializing Function Parameters

With all the functions we have used up to now, we have had to take care to provide an argument corresponding to each parameter in a function call. It can be quite handy to be able to omit one or more arguments in a function call, and have some default values, for the arguments that you leave out, supplied automatically. You can arrange this by initializing the parameters to a function in its prototype.

For example, let's suppose that we write a function to display a message, where the message to be displayed is to be passed as an argument. Here is the definition of such a function:

void showit(char* message)
{
   cout << endl
        << message;
   return;
}

We can initialize the parameter to this function by specifying the initializing string value in the function prototype, as follows:

void showit(char* message = "Something is wrong.");

Here, the parameter message is initialized with the string shown. If you've initialized a parameter to a function in the prototype, and if you leave out that argument when you call the function, the initializing value is used in the call.

Try It Out - Omitting Function Arguments

Leaving out the function argument when you call the function will execute it with the default value. If you supply the argument, it will replace the default value. We can use the previous function to output a variety of messages.

//Ex5_03.cpp
// Omitting function arguments
#include <iostream>
using namespace std;

void showit(char* = "Something is wrong.");

int main()
{
   char* mymess = "The end of the world is nigh.";
   showit();                   // Display the basic message
   // Display an alternative
   showit("Something is terribly wrong!");
   showit();                   // Display the default again
   showit(mymess);             // Display a predefined message
   cout << endl;
   return 0;
}

void showit(char* message)
{
   cout << endl
        << message;
   return;
}

If you execute this example, it will produce the following apocalyptic output:

How It Works

As you can see, we get the default message specified in the function prototype whenever the argument is left out. Otherwise, the function behaves normally.

If you have a function with several arguments, you can provide initial values for as many of them as you like. If you want to omit more than one argument to take advantage of a default value, all arguments to the right of the leftmost argument that you omit must also be left out. For example, if you have this function,

int do_it(long arg1=10, long arg2=20,
          long arg3=30, long arg4=40);

and you want to omit one argument in a call to it, you can omit only the last one, arg4. If you want to omit arg3, you must also omit arg4. If you omit arg2, arg3 and arg4 must also be omitted, and if you want to use the default value for arg1, you have to omit all of the arguments in the function call.

You can conclude from this that you need to put the arguments which have default values in the function prototype together in sequence at the end of the parameter list, with the argument most likely to be omitted appearing last.


(Exceptions)

Exceptions

An exception can be loosely described as an event that you did not expect to occur. Error conditions that arise in your program are exceptions, but any unusual condition that arises, such as erroneous input, could also be considered as an exception. So far, we have typically handled error conditions by using an if statement to test some expression, and then executing some specific code to deal with the error. C++ also provides another, more general mechanism for handling exceptions which allows you to separate the code that deals with these conditions from the code that executes when such conditions do not arise. This mechanism uses three new keywords:

  • try - identifies a code block in which an exception can occur

  • throw - causes an exception condition to be originated

  • catch - identifies a block of code in which the exception is handled

Try It Out - Throwing and Catching Exceptions

We can see how exception handling works by working through an example. Let's use a very simple context for this. Suppose we write a program to read the height of a person in inches, and then display the height in meters. Let's also suppose that we want to validate the height entered so that we won't accept any values greater than 100 inches, or less than 9 inches.

We could code this using exception handling as follows:

// Ex5_04.cpp  Using exception handling
#include <iostream>
using namespace std;

int main()
{
   int Height = 0;
   double InchesToMeters = 0.0254;
   char ch = 'y';

   while(ch == 'y'||ch =='Y')
   {
      cout << "Enter a height in inches: ";
      cin >> Height;        // Read the height to be converted

      try                   // Defines try block in which
      {                     // exceptions may be thrown
         if(Height > 100)
            throw "Height exceeds maximum";   // Exception thrown
         if(Height < 9)
            throw "Height below minimum";     // Exception thrown
         cout << static_cast<double>(Height*InchesToMeters)
              << " meters" << endl;
         cout << "Do you want to continue (y or n)?";
         cin >> ch;
      }

      // start of catch block to catch exceptions of type char*
      catch(const char* aMessage) //
      {                      //
         cout << aMessage << endl;
      }
   }

   return 0;
}

If you run this example, typical output will be as shown below:

How It Works

The code in the try block is executed in the normal sequence. The try block serves to define where an exception can be raised. You can see from the output that when an exception is thrown, the catch block is executed, and execution continues with the statement following the catch block. When no exception is thrown, the catch block is not executed. Because the try block encloses the statement to output the converted value of Height and read a value for ch, and because these follow the throw statements, they are not executed when an exception is thrown. When a throw statement is executed, control passes immediately to the first statement in the catch block.

Throwing Exceptions

Exceptions can be thrown anywhere within a try block. In our example, we throw two exceptions in the throw statements that you see. The operand of the throw statements determines a type for the exception - both the exceptions thrown here are of type const char*. The operand following the throw keyword can be any expression and the type of the result of the expression determines the type of exception thrown.

Exceptions can also be thrown in functions called from within a try block and caught by a catch block following the try block. You could add a function to the previous example to demonstrate this, with the definition:

void TestThrow(void)
{
   throw "An exception from within a function!";
}

If you place a call to this function after the statement reading the value for ch, this exception will be thrown and caught by the catch block on every iteration when no other exception is thrown. Don't forget the function prototype if you add the definition of TestThrow() to the end of the source code.

Catching Exceptions

The catch block following the try block in our example catches any exception of type const char*. This is determined by the exception declaration that appears in parentheses following the keyword catch. You must supply at least one catch block for a try block, and the catch blocks must immediately follow the try block. A catch block will catch all exceptions (of the correct type) that occur anywhere in the code in the immediately preceding try block, including those thrown in any functions that are called directly or indirectly within the try block.

If you want to specify that a catch block is to handle any exception that is thrown in a try block, you must put an ellipsis, ..., between the parentheses enclosing the exception declaration:

catch (...)
{
   // code to handle any exception
}

This catch block must appear last if you have other catch blocks defined for the try block.

Try It Out - Nested try Blocks

You can nest try blocks one within another. With this situation, if an exception is thrown from within an inner try block which is not followed by a catch block corresponding to the type of exception thrown, the catch handlers for the outer try block will be searched. You can demonstrate this by modifying the previous example as follows:

// Ex5_05.cpp
// Nested try blocks
#include <iostream>
using namespace std;
int main()
{
   int Height = 0;
   double InchesToMeters = 0.0254;
   char ch = 'y';

   try                       // Outer try block
   {
      while(ch == 'y'||ch =='Y')
      {
         cout << "Enter a height in inches: ";
         cin >> Height;        // Read the height to be converted

         try                   // Defines try block in which
         {                     // exceptions may be thrown
            if(Height > 100)
               // Exception thrown
               throw "Height exceeds maximum";
            if(Height < 9)
               throw Height; // Exception thrown
            cout << static_cast<double>(Height*InchesToMeters)
                 << " meters" << endl;
            cout << "Do you want to continue(y or n)?";
            cin >> ch;
         }

         catch(char* aMessage) // start of catch block to catch
         {                     // exceptions of type char*
            cout << aMessage << endl;
         }
      }
   }

   catch(int BadHeight)
   {
      cout << BadHeight << " inches is below minimum" << endl;
   }

   return 0;
}

Here, there is an extra try block enclosing the while loop and the second exception thrown in the inner try block has been changed to throw the value of Height when this value is below the minimum. If you run this version of the program, the exception of type char* is caught by the catch block for the inner try block. The exception of type int has no catch handler for exceptions of this type, so the catch handler for the outer try block is executed. In this case, the program ends immediately because the statement following the catch block is a return.


(Function Overloading)

Function Overloading

Suppose we have a function which generates the maximum value of an array of values of type double:

// Function to generate the maximum value in an
// array of type double
double maxdouble(double array[], int len)
{
   double max = array[0];
   for(int i=1; i<len; i++)
      if(max<array[i])
         max = array[i];
   return max;
}

We now want to create a function which produces the maximum value from an array of type long, so we write another function very similar to the first, with this prototype:

long maxlong(long array[], int len);

We now have to be careful to choose the appropriate function name to match the particular task in hand. We may also need the same function for other types of argument. It seems a pity that we have to keep inventing new names. Ideally, we would want to use the function max() for whatever type, and have the appropriate version executed.

The mechanism which enables you to do this is called function overloading.

What is Function Overloading?

Function overloading allows you to use the same name in different functions and, in each instance, to have the compiler choose the correct version for the job. There has to be a clear method for the compiler to decide which function is to be called in any particular instance. The key to this is the parameter list. A series of functions with the same name, but differentiated by their parameter lists, is a set of overloaded functions. So, following on from our max() function example, we could have overloaded functions with the following prototypes:

int max(int array[], int len);         // Prototypes for
long max(long array[], int len);       // a set of overloaded
double max(double array[], int len);   // functions

Each of the functions that share a common name must have a different parameter list. Note that a different return type does not distinguish a function adequately. You can't add the function,

double max(long array[], int len);

to the above set as it would clash with this prototype,

long max(long array[], int len);

causing the compiler to complain and the program not to compile. This may seem slightly unreasonable, until you remember that you can write statements such as these:

long numbers[] = {1,2,3,3,6,7,11,50,40};
int len = sizeof numbers/sizeof number[0];
...
max(numbers, len);

If the return type were permitted as a distinguishing feature, the version of max() taking a long array as an argument and returning a double value would be allowed, along with the original three. In the instance of the code above, the compiler would be unable to decide whether to choose the version with a long return type or a double return type. You could however replace the long function with the double function in the set, as the parameter list for each function in the set would still be unique.

Each function in a set of overloaded functions is sometimes said to have a unique signature, which is determined by the parameter list.

Try It Out - Using Overloaded Functions

We can exercise the overloading capability with the function max() that we have already defined. Let's try an example that includes the three versions for int, long and double arrays.

// Ex5_06.cpp
// Using overloaded functions
#include <iostream>
using namespace std;

int max(int array[], int len);         // Prototypes for
long max(long array[], int len);       // a set of overloaded
double max(double array[], int len);   // functions

int main()
{
   int small[] = { 1,24,34,22};
   long medium[] = { 23,245,123,1,234,2345};
   double large[] = { 23.0,1.4,2.456,345.5,12.0,21.0};
   int lensmall = sizeof small/sizeof small[0];
   int lenmedium = sizeof medium/sizeof medium[0];
   int lenlarge = sizeof large/sizeof large[0];
   cout << endl << max(small, lensmall);
   cout << endl << max(medium, lenmedium);
   cout << endl << max(large, lenlarge);
   cout << endl;
   return 0;
}

// Maximum of ints
int max(int x[], int len)
{
   int max = x[0];
   for(int i=1; i<len; i++)
      if(max<x[i])
         max = x[i];
   return max;
}

// Maximum of longs
long max(long x[], int len)
{
   long max = x[0];
   for(int i=1; i<len; i++)
      if(max<x[i])
         max = x[i];
   return max;
}

// Maximum of doubles
double max(double x[], int len)
{
 double max = x[0];
   for(int i=1; i<len; i++)
      if(max<x[i])
         max = x[i];
   return max;
}

How It Works

We have three prototypes for the three overloaded versions of the function max(). In each of the three output statements, the appropriate version of the function max() is selected by the compiler based on the argument list types. The example works as expected and produces this output:

When to Overload Functions

Function overloading provides you with the means of ensuring that a function name describes the function being performed, and is not confused by extraneous information such as the type of data being processed. This is akin to what happens with basic operations in C++. To add two numbers you use the same operator, regardless of the types of the operands. Our overloaded function max() has the same name, regardless of the type of data being processed. This helps to make the code more readable and makes these functions easier to use.

The intent of function overloading is clear: to enable the same operation to be performed with different operands using a single function name. So, whenever you have a series of functions that do essentially the same thing, but with different types of arguments, you should overload them and use a common function name.


(Function Templates)

Function Templates

The last example was somewhat tedious in that we had to repeat essentially the same code for each function, but with different variable and parameter types. However, we also have the possibility of having a recipe for automatically generating functions of various types. The code to do this for a particular group of functions is called a function template.

The functions generated by a function template all have the same basic code, but have one or more types defined as parameters to the template. As you use a particular form of a template function, a version is automatically generated to support the type of arguments that you use. We can demonstrate this by defining a function template for the function max() in the previous example.

Using a Function Template

We can define a template for the function max() as follows:

template<class T> T max(T x[], int len)
{
   T max = x[0];
   for(int i=1; i<len; i++)
      if(max<x[i])
         max = x[i];
   return max;
}

The template keyword identifies this as a template definition. The angled brackets following the template keyword enclose the type parameters that are used to create a particular instance of the function. The keyword class before the T indicates that the T is the type parameter for this template, class being the generic term for type. We shall see later in the book that defining a class is essentially defining your own data type. Consequently, you have basic types in C++, such as int and char, and you also have the types that you define yourself.

Wherever T appears in the definition of the template, it is to be replaced by a specific type, such as long, when an instance of the template is created. If you try this out manually, you'll see that this will generate a perfectly satisfactory function for calculating the maximum value from an array of type long. The creation of a particular function instance is referred to as instantiation. In our case, we have just one type parameter, T, but in general, there can be more.

Each time you use the function max() in your program, the compiler will check to see if a function corresponding to the type of arguments that you have used in the function call already exists. If the required function does not exist, the compiler will create one by substituting the argument type that you have used in place of the parameter T throughout the source code in the template definition. You could exercise this template with the same function main() that we used in the previous example:

// Ex5_07.cpp
// Using function templates
#include <iostream>
using namespace std;

// Template for function to compute the
// maximum element of an array
template<class T> T max(T x[], int len)
{
   T max = x[0];
   for(int i=1; i<len; i++)
      if(max<x[i])
         max = x[i];
   return max;
}

int main()
{
   int small[] = { 1,24,34,22};
   long medium[] = { 23,245,123,1,234,2345};
   double large[] = { 23.0,1.4,2.456,345.5,12.0, 21.0};
   int lensmall = sizeof small/sizeof small[0];
   int lenmedium = sizeof medium/sizeof medium[0];
   int lenlarge = sizeof large/sizeof large[0];
   cout << endl << max(small, lensmall);
   cout << endl << max(medium, lenmedium);
   cout << endl << max(large, lenlarge);
   cout << endl;
   return 0;
}

If you run this program, it will produce exactly the same output as the previous example. For each of the statements outputting the maximum value in an array, a new version of max() is instantiated using the template. Of course, if you add another statement calling the function max() with one of the types used previously, no new version of the code is generated.

Note that using a template doesn't reduce the size of your compiled program in any way. A version of the source code is generated for each function that you require. In fact, using templates can generally increase the size of your program, as functions can be created automatically even though an existing version might satisfactorily be used by casting the argument accordingly. You can force the creation of particular instances of a template by explicitly including a declaration for it. For example, if you wanted to ensure that an instance of the template for the function max() was created corresponding to the type float, you could place the following declaration after the definition of the template:

float max(float, int);

This will force the creation of this version of the function template. It does not have much value in the case of our program example; but it can be useful when you know that several versions of a template function might be generated, but you want to force the generation of a subset that you plan to use with arguments cast to the appropriate type where necessary.


(Working with Program Files)

Working with Program Files

All the examples we have seen so far have been fairly simple programs in just one file, but in general, your C++ programs will consist of multiple files. Two basic kinds of files are involved:

  • Header files, which are commonly identified with the file name extension .h (note that some older systems use .hpp)

  • Source files, which usually have the extension .cpp, and will contain your code that will compile to machine instructions - primarily function definitions.

As we have seen, the standard header files for library functions in ANSI C++ (for example, iostream) have no extension. Of course, there may be other kinds of files supporting the environment in which you are programming (defining resources of one kind or another, perhaps) but the .h and .cpp files are the ones that will contain all your C++ code.

The essential difference between a header file and a .cpp file is in how each is used. By convention:

  • .cpp files contain code that determines what your program does (primarily function definitions) - that is, the logic of your program

  • .h files contain your function declarations (that is, the prototypes, but not the definitions), type definitions and pre-processor directives

  • You should avoid putting function definitions in a .h file

  • Each .cpp must #include any header files that it needs

  • When you compile your program, you compile just the .cpp files, which will incorporate the contents of the #included header files as necessary

Each .cpp file that you compile will produce an object file. The term 'object file' has nothing to do with class objects (we'll meet those in the next chapter). An object file is simply a file that contains the binary output from the compiler; it is often identified with the extension .obj. The object files are combined by the linker into a single executable module, that usually has the extension .exe.

So far, we have used only header files which provide the declarations necessary for using the standard library. Our program examples have been short and simple; consequently, they have not warranted the use of separate header files, which contain our function declarations or constant definitions. In Chapter 7, when we begin to define our own data types, the need for header files will become apparent. A typical practical C++ program will involve a number of header files and .cpp files.

External Variables

If your program consists of several files, the connection between a function call in one source file, and the function definition in another, is established (or resolved) by the linker. Before the linker acts, the compiler will compile a call to the function - to do this, it extracts the necessary information from the function prototype (which must occur before the function call is made). The compiler doesn't really mind whether the function's definition occurs in the same file, or in another .cpp file - the linker will sort it out.

Variable names are different. If you want to use a name to access a variable from within a function body, and the variable is defined outside the function body, then you must declare the variable using the extern keyword.

Accessing Variables in Another File

Let's see how this works by looking at an example. Suppose we have variables defined as global in a file file1.cpp using the statements:

double pi = 3.14159265;
int primes[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31};

These statements define the variable pi and the array days[], so there cannot be duplicate definitions for the same variables elsewhere. However, we can access these variables from another function in a different source file (or, for that matter, in the same source file), by declaring the variables as external. For example, a function in the file file2.cpp will contain the following statements:

extern double pi;       // Variable is defined in another file
extern int primes[];    // Array is defined in another file

These statements do not create the variables - they merely identify that they are defined elsewhere, so the compiler does not expect to find them within the present scope. The variables are used in file2.cpp, but their actual locations are determined by their definitions in file1.cpp. This connection will be established by the linker when the object files are linked together.

Of course, if you declare a name as external, and its corresponding definition is not found, then you will get an error message from the linker and no executable module will be created.

Accessing consts in Another File

In the above example, you'd probably want to avoid the possibility of these variables being altered. Therefore you might consider making each of them const when you define them in file1.cpp:

const double pi = 3.14159265;
const int primes[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31};

However, declaring them as const will have the effect of making them unavailable in other translation units. You can override this by using the extern keyword when you define them:

extern const double pi = 3.14159265;
extern const int primes[] = {2, 3, 5, 7,
                               11, 13, 17, 19, 23, 29, 31};

These statements still define the const double and the const char array. The use of the keyword extern here tells the compiler that these variable names should have external linkage, even though they are consts.

Now, when you want to access these in file2.cpp, you must declare them as const, as well as external:

extern const double pi; // Variable is defined in another file
extern const int primes[]; // Array is defined in another file

Within the block in which these declarations appear, the use of the names pi and primes will refer to the constants defined in the other file. Clearly, you can have as many declarations for external variables as you want. Of course, any external variable of a given type can only have one definition in a program.

Global variables can be useful for constant values that you want to share. Indeed, by sharing constant values across all of the program files that need access to them, you can ensure that the same values are being used for the constants throughout your program. However, in general you should be aware of the disadvantages in declaring a variable as global. If your function uses a global variable, then it has an external dependency, which means that it is no longer a nice clean self-contained package of code. This makes for messy program structure, and limits the degree to which your function can be reused in another program.

Global variables are always initialized. If you don't supply an initial value for a global variable, it will be initialized with 0.


(Namespaces)

Namespaces

With large programs, choosing unique names for all the entities can become difficult. This is particularly true when an application is being developed by several programmers working in parallel, or when a vendor wants to produce a library for sale to third parties. Without some kind of mechanism to prevent it, name clashes become highly likely. This is perhaps most likely in the context of user-defined types, or classes, which we will meet in the next chapter. A namespace is designed to overcome this difficulty.

We first met the concept of a namespace in Chapter 1. Now, we're going to have a look at them in a bit more detail. A namespace is a region within a program that attaches an extra name - a namespace name - to all the entity names within it. Two different namespaces can each contain entities with the same name, but the entities will be differentiated because a different namespace name is attached to each of them. The diagram illustrates two namespaces defined within a single program, possibly within the same source file.

In each namespace, there is a variable defined with the name i, and a function declared with the name max(). However, they each refer to distinct entities, with no possibility of a clash between them. You would typically use a separate namespace name within a single program for each collection of code that encompasses a common purpose. Each namespace would represent some logical grouping of functions, together with any related global variables and declarations. A namespace would also be used to completely contain a unit of release, such as a library.

You are already aware that the standard library is defined within the namespace std. This implies that every external name in the standard library is prefixed with std. For instance, the output stream name is std::cout, so to display the contents of a variable, value, you could write:

std::cout << std::endl << value;

The operator :: here is the binary form of the scope resolution operator, that we saw earlier. The statement above qualifies the names cout and endl - it says to the compiler, "You'll find the definition of these names in the std namespace." Of course, you will immediately appreciate that this is not what we have be doing in the examples so far - this is because we have been adding the following statement to our programs:

using namespace std;

In principle, this using directive allows all the names in the namespace, std, to be used without qualification. Of course, any name that you want to use must appear in a declaration prior to where you want to use it. So in this case, the names in the std namespace that you can use will be those in the standard header files that have been included into the file.

The using directive allows the appearance of your code to be simplified somewhat, and in all our examples so far, that has been our motivation for employing the using directive. However, you need to be cautious when doing this. If you employ the using directive for all your namespaces, it defeats the purpose of using namespaces at all. We will come back to the using directive later in this discussion.

The Global Namespace

All the programs that we have written so far have used names that we declared in the global namespace. The global namespace applies by default in the absence of a namespace being defined. All names within the global namespace are just as you declare them, without a namespace name being attached. In a program with multiple source files, all the names with linkage are within the global namespace.

With small programs, you can define your names within the global namespace without running into any problems. With larger applications, the potential for name clashes increases so you should use namespaces to partition your code into logical groupings. That way, each code segment will be self-contained from a naming perspective, and name clashes will be prevented.

Of course, you need to know how to declare a namespace, so let's look at that next.

Declaring a Namespace

You can declare a namespace with the statements:

namespace myRegion
{
  // Code you want to have in the namespace, including
  // function definitions and declarations, global variables,
  // templates, etc.
}

Note that, at the end of the namespace declaration, there is no semi-colon following the closing brace.

The namespace name assigned here is myRegion. This uniquely identifies the namespace and this name will be attached to all the entities declared within the namespace. The braces enclose the scope for the namespace myRegion, and every name within the namespace scope will have the name myRegion attached to it.

You must not include the function main() within the namespace. The runtime environment expects main() to be defined in the global namespace, so you must always place the function main() outside of all your namespaces.

You can extend a namespace scope by adding a second namespace definition in a file. For example, a program file might contain:

namespace calc
{
  // This defines namespace calc
  // The initial code in the namespace goes here
}

namespace sort
{
  // Code in a new namespace, sort
}

namespace calc
{
  // This extends the namespace calc
  // Code in here can refer to names in the previous
  // calc namespace block without qualification
}

Here, we have two blocks declared as namespace calc, separated by a namespace sort. The second calc block is treated as a continuation of the first. Therefore, functions defined within each of the calc blocks all belong to the same namespace. The second block is referred to as an extension namespace definition, because it extends the original namespace definition. You could have further extension namespace definitions in the file, adding more code within the same namespace.

Of course, you would not usually organize a program file in this way directly. However, if you #include several header files into your program file, and each header file contributes some code to the same namespace, then you effectively have the sort of situation described above. A common example of this is when you #include a number of standard library headers (each of which contributes to the namespace std), interspersed with header files of your own (defined within a different namespace). For example:

#include <iostream>      // In namespace std
#include "mystuff.h"     // In my namespace calc
#include <string>
// In namespace std - extension namespace definition
#include "morestuff.h"
// In my namespace calc - extension namespace definition

Finally, note that references to names within a namespace from inside the same namespace do not need to be qualified. For example, names that belong to the namespace calc can be referenced from within calc, without the need to qualify them with the namespace name.

Let's take a simple example to illustrate the mechanics of declaring and using a namespace.

Try It Out - Using a Namespace

We will create a program consisting of two .cpp files. The first will just contain definitions of some consts that we defined earlier, but this time defined within a namespace:

// Data5_08.cpp
// Using a namespace
namespace data
{
  extern const double pi = 3.14159265;
  extern const int primes[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31};
}

Here we have defined pi and primes[] within the namespace data. We will use these variables in another translation unit, which will contain main():

// Ex5_08.cpp
// Using a namespace
#include <iostream>

namespace data
{
  extern const double pi; // Variable is defined in another file
  extern const int primes[]; // Array is defined in another file
}

int main()
{
  std::cout << std::endl
            << "pi has the value "
            << data::pi << std::endl;
  std::cout << "The forth prime number is "
            << data::primes[3] << std::endl;
  return 0;
}

If you compile and run this it will produce the output:

How It Works

In the file containing main(), we must declare pi and days[] as external, since they are defined in a separate translation unit. We do this with the statements:

namespace data
{
  extern const double pi; // Variable is defined in another file
  extern const int primes[]; // Array is defined in another file
}

We have to place the declarations for the external variables within the namespace data, because the variables are defined within this namespace in the first .cpp file. This demonstrates the point that we discussed earlier - that a namespace can be defined piecemeal. Even within a single file, there can be several namespace blocks corresponding to the same namespace name, and the contents of each will be in the same namespace.

As a general rule, you should put these declarations for pi and primes in a header file, data.h say. The contents of this header file would just be:

// data.h
// Declarations for globals in namespace data
namespace data
{
  extern const double pi; // Variable is defined in another file
  extern const int primes[]; // Array is defined in another file
}

Then, to make the declarations available in the file containing main() (or any other file that needed access to these variables), you would simply need to add an #include directive at the beginning:

#include "data.h"

The name has the extension .h to identify that it is a header file. We'll see this in action in Ex5_09.

The syntax for #include is slightly different here. You should omit the .h extension only when including standard library headers.

Back in Ex5_08, the code in the body of main() just consists of two output statements:

std::cout << std::endl
          << "pi has the value "
          << data::pi << std::endl;
std::cout << "The forth prime number is "
          << data::primes[3] << std::endl;

The function main() is in the global namespace (it must be, otherwise it won't be recognized), so any name used in main() that is declared and defined in another namespace must be qualified - either directly, or implicitly via a using directive (note that there is another way, which we will see in a moment). Since we do not have a using directive for the namespace std in the source file, we must qualify each name that is from the standard library. Thus cout and endl must be qualified with the namespace name std, so that they can be recognized by the compiler. The constants pi and primes are defined and declared within the namespace data, so these names must be qualified too.

Implicitly Qualifying Names

In principle, the using directive can make all the names in a namespace available. In the previous example, we could add a using directive for the namespace std, and avoid the necessity of qualifying cout and endl explicitly. The file contents would then be:

// Ex5_08.cpp
// Using a namespace
#include <iostream>
namespace data
{
  extern const double pi; // Variable is defined in another file
  extern const int primes[];// Array is defined in another file
}

using namespace std;

int main()
{
  cout << endl
       << "pi has the value "
       << data::pi << endl;
  cout << " The forth prime number is "
       << data::primes[3] << endl;
  return 0;
}

Now, we don't need to qualify cout and endl, although we still could if we wanted to. You would need to do this if you had a name of your own that clashed with a name in the standard library namespace. It would be silly to do so, but to illustrate the point you could write:

int cout = 10;
std::cout << Value of variable cout is " << cout;

In this code fragment, there is a variable cout defined here of type int, which is contained in the global namespace. There is also an output stream, cout, but this belongs to the std namespace. In the second line above, cout is understood to refer to the global variable, which effectively masks the output stream cout, even if we have a using directive. You can still get at the output stream cout by using the namespace name as a qualifier, as shown in the second statement.

When you use an unqualified variable name, the compiler will first try to find the definition of the variable in the current scope, prior to the point at which it is used. If it is not found, then it will look in the immediately enclosing scope. This will continue until the global scope is reached. If a declaration for the variable is not found at global scope (which could be an extern declaration), the compiler will conclude that the variable is not defined.

By adding a using directive for the namespace data just before main() in the last example, we could obviate the need to qualify any of the names, so the file contents would be:

// Ex5_08.cpp
// Using a namespace
#include <iostream>
namespace data
{
  extern const double pi; // Variable is defined in another file
  extern const int primes[]; // Array is defined in another file
}

using namespace std;
using namespace data;

int main()
{
  cout << endl
       << "pi has the value "
       << pi << endl;
  cout << " The forth prime number is "
       << primes[3] << endl;
  return 0;
}

In this case, this makes the code much simpler. If, later, we developed this program so that std and data namespaces both contained a certain name, then we would resolve the ambiguity in the program by using the scope resolution operator. Of course, if there were lots of ambiguities then we'd lose the simplicity of the code again, even with the using directives. An alternative approach, which sits nicely between these extremes, is to use using declarations rather than using directives.

Applying using Declarations

A using declaration declares a specific name from within a given namespace. Each using declaration declares a single name, which can then be used unqualified following the declaration. A using declaration is of the form:

using namespace_name::identifier;

where using is a keyword, namespace_name is the name of the namespace and identifier is the name that we want to use unqualified. Note that this introduces a name from the namespace, which might refer to several different things. For instance, a set of overloaded functions defined within a namespace can be introduced with a single using declaration.

Instead of the using directive for the namespace std in the previous example, we could supply a using declaration for each name that we want to use:

using std::cout;
using std::endl;

These two declarations, preceding the definition of main() in the last example, would allow cout and endl to be used without qualification. Of course, you could also supply using declarations for the constants pi and primes in the namespace data.

Note that although we have placed the using declarations and directives at global scope in all our examples, we can also place them within a namespace, within a function, or even within a statement block. In each case the declaration or directive applies until the end of the block that contains it.

Functions and Namespaces

For a function to exist within a namespace, it is sufficient for the function prototype to appear in a namespace. The function can be defined elsewhere, by simply using the qualified name for the function - in other words, the function definition doesn't have to be enclosed in a namespace block. Let's take an example.

Suppose we want to write two functions, max() and min(), to return the maximum and minimum of an array of values. We can put the declarations for the functions in a namespace as follows:

// compare.h
namespace compare
{
  double max(const double* data, int size);
  double min(const double* data, int size);
}

This could be placed in a header file, compare.h, which would then be #included by any file using the functions.

The definitions for the functions can now appear in a .cpp file. We can write the definitions without enclosing them in a namespace block, as long as the name of each function is qualified with the namespace name. The contents of the file would be:

// compare.cpp
#include "compare.h"

// Function to find the maximum
double compare::max(const double* data, int size)
{
  double result = data[0];
  for(int i = 1 ; i < size ; i++)
    if(result < data[i])
      result = data[i];
  return result;
}

// Function to find the minimum
double compare::min(const double* data, int size)
{
  double result = data[0];
  for(int i = 1 ; i < size ; i++)
    if(result > data[i])
      result = data[i];
  return result;
}

We need the compare.h header file to be included so that the namespace is identified. This enables the compiler to deduce that the functions are within the namespace.

Of course you could, if you want, place the code for the function definitions within the compare namespace directly, and you would not have to qualify each function name with the name of the namespace. Therefore, if we were to write the function definitions in this way, we would no longer need to #include the file compare.h.

Using the functions is the same, however you have defined them. To confirm how easy it is, let's try it out with the functions that we have just defined.

Try It Out - The using Declaration

The function declarations go in the header file compare.h as we discussed above:

// compare.h
namespace compare
{
  double max(const double* data, int size);
  double min(const double* data, int size);
}

We will put the definitions for the functions in a separate .cpp file that will contain the following code:

// compare.cpp
#include "compare.h"

// Function to find the maximum
double compare::max(const double* data, int size)
{
  double result = data[0];
  for(int i = 1 ; i < size ; i++)
    if(result < data[i])
      result = data[i];
  return result;
}

// Function to find the minimum
double compare::min(const double* data, int size)
{
  double result = data[0];
  for(int i = 1 ; i < size ; i++)
    if(result > data[i])
      result = data[i];
  return result;
}

All we need is a .cpp file containing the definition of main() to try the functions out:

// Ex5_09.cpp
// Using functions in a namespace
#include <iostream>
#include "compare.h"

using compare::max;    // Using declaration for max
using compare::min;    // Using declaration for min

int main()
{
  double data[] = {1.5, 4.6, 3.1, 1.1, 3.8, 2.1};
  using namespace std; // Using directive for standard library
  const int dataSize = sizeof data/sizeof data[0];
  cout << endl;
  cout << "Minimum double is " << min(data, dataSize) << endl;
  cout << "Maximum double is " << max(data, dataSize) << endl;
  return 0;
}

If you compile the two .cpp files and link them, when you run the program it will produce the output:

How It Works

The declarations for the functions are introduced into the file containing main() with the directive:

#include "compare.h"

If the file compare.h was in a different directory to the source files, then the #include directive must also contain the directory path from the source file to the header file. In this example, the header file compare.h is in the same directory as the source file.

We then have a using declaration for each function, so that we can use the names without having to add the namespace name:

using compare::max;    // Using declaration for max
using compare::min;    // Using declaration for min

We could equally well have used a using directive for the compare namespace:

using namespace compare;

Since the namespace only contains the functions max() and min(), this would have been just as good, and one less line of code. However, in general the effect of a using directive is quite different from the effect of a using declaration. The using directive allows any name from the namespace that is declared in the file to be used. The using declaration on the other hand only introduces one specific name.

Within main() we first define an array, data, that we will pass as an argument to our two functions:

double data[] = {1.5, 4.6, 3.1, 1.1, 3.8, 2.1};

We then have a using directive for the namespace std that allows us to use the names declared in <iostream> without qualification from this point on:

using namespace std;    // Using directive for standard library

We will need to pass the length of the array, data, to each function, so we compute this and store it in the constant dataSize:

const int dataSize = sizeof data/sizeof data[0];

Finally, we call the functions in the output statements which will display the maximum and minimum values from the array, data:

cout << "Minimum double is " << min(data, dataSize) << endl;
cout << "Maximum double is " << max(data, dataSize) << endl;

If we had not written the using declarations for the function names (or a using directive for the compare namespace), then we would have had to qualify the functions in the output statements:

cout << "Minimum double is "
     << compare::min(data, dataSize) << endl;
cout << "Maximum double is "
     << compare::max(data, dataSize) << endl;


(Summary)

Summary

You now have a reasonably comprehensive knowledge of writing and using functions. You've used a pointer to a function, learnt about exceptions and you have used overloading to implement a set of functions providing the same operation with different types of parameters. We'll see more about overloading functions in the following chapters. We've also looked at function templates, working with program files and namespaces

The important bits that you learned in this chapter are:

  • A pointer to a function stores the address of a function, plus information about the number and types of parameters and return type for a function.

  • You can use a pointer to a function to store the address of any function with the appropriate return type, and number and types of parameters.

  • You can use a pointer to a function to call the function at the address it contains. You can also pass a pointer to a function as a function argument.

  • Overloaded functions are functions with the same name, but with different parameter lists.

  • When you call an overloaded function, the function to be called is selected by the compiler based on the number and types of the arguments that you specify.

  • A function template is a recipe for generating overloaded functions automatically.

  • A function template has one or more arguments that are type variables. An instance of the function template - that is, a function definition - will be created by the compiler for each function call that corresponds to a unique set of type arguments for the template.

  • You can force the compiler to create a particular instance from a function template by specifying the function you want in a prototype declaration.

  • Exceptions help separate error-handling code from your main program logic, and can provide specific information on the cause of the problem.

  • C++ programs consist of header and source files. When the code is compiled, the relevant header files are called into the source code files, and each source code file is compiled to an object file. These object files are then linked to form a single executable.

  • In order to reuse variables between source code files, you must make use of the extern keyword.

  • Namespaces are a way to avoid clashes between non-unique variable and function names in C++. You specify a namespace to which your code belongs, which is then used to qualify the name.

  • To refer to code that belongs to a name space, you can use using directives and declarations, or refer to it directly using the scope resolution operator.


(Structuring Your Data Using Classes)

Chapter 6 - Structuring Your Data Using Classes

This chapter is all about creating your own data types to suit your particular problem. We'll learn how to create objects - the building blocks of object-oriented programming. An object can seem a bit mysterious to the uninitiated but, as we shall see in this chapter, an object is just an instance of one of your own data types.

In this chapter you will learn about:

  • Classes and how they are used

  • The basic components of a class and how a class is declared

  • Creating and using objects of a class

  • Controlling access to members of a class

  • Constructors and how to create them

  • The default constructor

  • References in the context of classes

  • The copy constructor and how it is implemented


(Structuring Your Data Using Classes)

Data Types, Objects, Classes and Instances

So far, we've learnt that C++ lets you create variables which can be any of a range of basic data types: int, long, double and so on. However, the variables of the basic types don't allow you to model real-world objects (or even imaginary objects) adequately. It's hard to model a box in terms of an int, for example; what we need is something that allows us to collect together the various attributes of an object. In C++, we can do this very easily using classes.

You could define variables, length, breadth and height to represent the dimensions of the box and bind them together as members of a Box class, as follows:

class Box
{
public:
   double length;
   double breadth;
   double height;
};

This code actually creates a new type, called Box. The keyword class defines Box as such, and the elements that make up this class are defined within the curly braces. Note that each line defining an element of the class is terminated by a semicolon, and that a semicolon also appears after the closing brace. The elements length, breadth and height are referred to as data members. At the top of the class definition, you can see we have put the keyword public - this just means that the data members are generally accessible from outside the class. You may, however, place restrictions on the accessibility of class members, and we'll see how this works a bit later in this chapter.

With this definition of a new data type called Box, you can go ahead and define variables of this type just as you did with variables of the basic types:

Box myBox;              //Declare a variable myBox of type Box

Once we've defined the class Box, the declaration of variables of this type is quite standard. The variable myBox here is also referred to as an object or an instance of the class Box.

With this definition of a new data type called Box, you can go ahead and define variables of this type just as you did with variables of the basic types. You can then create, manipulate and destroy as many Box objects as you need to in your program. This means that you can model objects using classes and write your programs around them. So - that's object-oriented programming all wrapped up then?

Well, not quite. You see, object-oriented programming (OOP) is based on a number of foundations (famously encapsulation, polymorphism and inheritance) and what we have seen so far doesn't quite fit the bill. Don't worry about what these terms mean for the moment - we'll be exploring these ideas in more detail as we learn more about what you can do with classes.

First Class

The notion of class was invented by an Englishman to keep the general population happy. It derives from the theory that people who knew their place and function in society would be much more secure and comfortable in life than those who did not. The famous Dane, Bjarne Stroustrup, who invented C++, undoubtedly acquired a deep knowledge of class concepts while at Cambridge University in England, and appropriated the idea very successfully for use in his new language.

The idea of a class in C++ is similar to the English concept, in that each class usually has a very precise role and a permitted set of actions. However, it differs from the English idea, because class in C++ has largely socialist overtones, concentrating on the importance of working classes. Indeed, in some ways it is the reverse of the English ideal, because, as we shall see, working classes in C++ often live on the backs of classes that do nothing at all.

Operations on Classes

In C++ you can create new data types as classes to represent whatever kinds of objects you like. As you'll come to see, classes aren't limited to just holding data; you can also define member functions that act on your objects, or even operations that act between objects of your classes using the standard C++ operators. You can define the class Box, for example, so that the following statements work and have the meanings you want them to have:

Box Box1;
Box Box2;
if(Box1 > Box2)       // Fill the larger box
   Box1.Fill();
else
   Box2.Fill();

You could also implement operations as part of the Box class for adding, subtracting or even multiplying boxes - in fact, almost any operation to which you could ascribe a sensible meaning in the context of boxes.

We're talking about incredibly powerful medicine here and it constitutes a major change in the approach that we can take to programming. Instead of breaking down a problem in terms of what are essentially computer-related data types (integer numbers, floating point numbers and so on) and then writing a program, we're going to be programming in terms of problem-related data types, in other words classes. These classes might be named Employee, or Cowboy, or Cheese or Chutney, each defined specifically for the kind of problem that you want to solve, complete with the functions and operators that are necessary to manipulate instances of your new types.

Program design now starts with deciding what new application-specific data types you need to solve the problem in hand and writing the program in terms of operations on the specifics that the problem is concerned with, be it Coffins or Cowpokes.

Terminology

Let's summarize some of the terminology that we will be using when discussing classes in C++:

  • A class is a user-defined data type

  • Object-oriented programming is the programming style based on the idea of defining your own data types as classes

  • Declaring an object of a class is sometimes referred to as instantiation because you are creating an instance of a class

  • Instances of a class are referred to as objects

  • The idea of an object containing the data implicit in its definition, together with the functions that operate on that data, is referred to as encapsulation

When we get into the detail of object-oriented programming, it may seem a little complicated in places, but getting back to the basics of what you're doing can often help to make things clearer, so always keep in mind what objects are really about. They are about writing programs in terms of the objects that are specific to the domain of your problem. All the facilities around classes in C++ are there to make this as comprehensive and flexible as possible. Let's get down to the business of understanding classes.


(Understanding Classes)

Understanding Classes

A class is a data type that you define. It can contain data elements, which can either be variables of the basic types in C++ or other user-defined types. The data elements of a class may be single data elements, arrays, pointers, arrays of pointers of almost any kind or objects of other classes, so you have a lot of flexibility in what you can include in your data type. A class can also contain functions which operate on objects of the class by accessing the data elements that they include. So, a class combines both the definition of the elementary data that makes up an object and the means of manipulating the data belonging to individual instances of the class.

The data and functions within a class are called members of the class. Funnily enough, the members of a class that are data items are called data members and the members that are functions are called function members or member functions. The member functions of a class are also sometimes referred to as methods, although we will not be using this term.

When you define a class, you define a blueprint for a data type. This doesn't actually define any data, but it does define what the class name means, that is, what an object of the class will consist of and what operations can be performed on such an object. It's much the same as if you wrote a description of the basic type double. This wouldn't be an actual variable of type double, but a definition of how it's made up and how it operates. To create a variable, you need to use a declaration statement. It's exactly the same with classes, as you will see.

Defining a Class

Let's look again at the class we started talking about at the start of the chapter - a class of boxes. We defined the Box data type using the keyword class as follows:

class Box
{
public:
   double length;            // Length of a box in inches
   double breadth;           // Breadth of a box in inches
   double height;            // Height of a box in inches
};

The name that we've given to our class appears following the keyword and the three data members are defined between the curly braces. The data members are defined for the class using the declaration statements that we already know and love, and the whole class definition is terminated with a semicolon. The names of all the members of a class are local to a class. You can therefore use the same names elsewhere in a program without causing any problems.

Access Control in a Class

The keyword public looks a bit like a label, but in fact it is more than that. It determines the access attributes of the members of the class that follow it. Specifying the data members as public means that these members of an object of the class can be accessed anywhere within the scope of the class object. You can also specify the members of a class as private or protected. In fact, if you omit the access specification altogether, the members have the default attribute, private. We shall look into the effect of these keywords in a class definition a bit later.

Remember that all we have defined so far is a class, which is a data type. We haven't declared any objects of the class. When we talk about accessing a class member, say height, we're talking about accessing the data member of a particular object, and that object needs to be declared somewhere.

Declaring Objects of a Class

We declare objects of a class with exactly the same sort of declaration that we use to declare objects of basic types. We saw this at the beginning of the chapter. So, we could declare objects of our class, Box, with these statements:

Box Box1;       // Declare Box1 of type Box
Box Box2;       // Declare Box2 of type Box

Both of the objects Box1 and Box2 will, of course, have their own data members. This is illustrated here:

The object name Box1 embodies the whole object, including its three data members. They are not initialized to anything, however - the data members of each object will simply contain junk values, so we need to look at how we can access them for the purpose of setting them to some specific values.

Accessing the Data Members of a Class

The data members of objects of a class can be referred to using the direct member access operator (.). So, to set the value of the data member height of the object Box2 to, say, 18.0, we could write this assignment statement:

Box2.height = 18.0;         // Setting the value of a data member

We can only access the data member in this way, in a function outside the class, because the member height was specified as having public access. If it wasn't defined as public, this statement would not compile. We'll see more about this shortly.

Try It Out - Your First Use of Classes

Let's have a go at creating a Box class and setting the values of it's data members. We'll try it out in the following console application:

// Ex6_01.cpp
// Creating and using boxes
#include <iostream>
using namespace std;

class Box              // Class definition at global scope
{
   public:
      double length;   // Length of a box in inches
      double breadth;  // Breadth of a box in inches
      double height;   // Height of a box in inches
};

int main(void)
{
   Box Box1;                // Declare Box1 of type Box
   Box Box2;                // Declare Box2 of type Box
   double volume = 0.0;     // Store the volume of a box here
   Box1.height = 18.0;      // Define the values
   Box1.length = 78.0;      // of the members of
   Box1.breadth = 24.0;     // the object Box1
   Box2.height = Box1.height - 10;  // Define Box2
   Box2.length = Box1.length/2.0;   // members in
   Box2.breadth = 0.25*Box1.length; // terms of Box1

// Calculate volume of Box1
   volume = Box1.height * Box1.length * Box1.breadth;
   cout << endl
        << "Volume of Box1 = " << volume;
   cout << endl
        << "Box2 has sides which total "
        << Box2.height + Box2.length + Box2.breadth
        << " inches.";
   cout << endl      // Display the size of a box in memory
        << "A Box object occupies "
        << sizeof Box1 << " bytes.";
   cout <<endl;

   return 0;
}

How It Works

Everything here works as we would have expected from our experience with structures. The definition of the class appears outside of the function main() and, therefore, has global scope. This enables objects to be declared in any function in the program and causes the class to show up in the ClassView once the program has been compiled.

We've declared two objects of type Box within the function main(), Box1 and Box2. Of course, as with variables of the basic types, the objects Box1 and Box2 are local to main(). Objects of a class obey the same rules with respect to scope as variables declared as one of the basic types (such as the variable volume, which is used in this example).

The first three assignment statements set the values of the data members of Box1. We define the values of the data members of Box2 in terms of the data members of Box1 in the next three assignment statements.

We then have a statement which calculates the volume of Box1 as the product of its three data members. This value is then output to the screen. Next, we output the sum of the data members of Box2 by writing the expression for the sum of the data members directly in the output statement. The final action in the program is to output the number of bytes occupied by Box1, which is produced by the operator sizeof.

If you run this program, you should get this output:

The last line shows that the object Box1 occupies 24 bytes of memory, which is a result of it having 3 data members of 8 bytes each. The statement which produced the last line of output could equally well have been written like this:

   cout << endl        // Display the size of a box in memory
        << "A Box object occupies "
        << sizeof (Box) << " bytes.";

Here, we've used the type name between parentheses, rather than a specific object name - this is standard syntax for the sizeof operator.

This example has demonstrated the mechanism for accessing the public data members of a class. It also shows that they can be used in exactly the same way as ordinary variables.

Using Pointers to Objects

As you might expect, we can create a pointer to variable of a Box type. The declaration for this is just what you might expect:

Box* pBox = &aBox

Assuming we have already declared an object, aBox, of type Box, then this line simply declares a pointer to type Box and initializes it with the address of aBox. We could now access the members of aBox through the pointer pBox, using statements like this:

(*pBox).length = 10;

The parenthesis to de-reference the pointer here are essential, since the member access operator takes precedence over the de-reference operator. Without the parenthesis, we would be attempting to treat the pointer like an object and to de-reference the member, so the statement would not compile.

This method of accessing a member of the class via a pointer looks rather clumsy. Since this kind of operation crops up a lot in C++, the language includes a special operator to enable you to express the same thing in a much more readable and intuitive form. It is called the indirect member access operator, and it is specifically for accessing members of an object through a pointer. We could use it to re-write the statement to access the length member of aBox through the pointer pBox:

pBox->length = 10;

The operator looks like a little arrow and is formed from a minus sign followed by the symbol for 'greater than'. It's much more expressive of what's going on, isn't it? We'll be seeing a lot more of this operator as we go along and learn more about classes. We're now ready to break new ground by taking a look at member functions of a class.

Member Functions of a Class

A member function of a class is a function that has its definition or its prototype within the class definition. It operates on any object of the class of which it is a member, and has access to all the members of a class for that object.

Try It Out - Adding a Member Function to Box

To see how accessing the members of the class works, let's create an example extending the Box class to include a member function.

// Ex6_02.cpp
// Calculating the volume of a box with a member function
#include <iostream>
using namespace std;

class Box                // Class definition at global scope
{
public:
   double length;        // Length of a box in inches
   double breadth;       // Breadth of a box in inches
   double height;        // Height of a box in inches
   // Function to calculate the volume of a box
   double Volume(void)
   {
      return length * breadth * height;
   }
};

int main(void)
{
   Box Box1;             // Declare Box1 of type Box
   Box Box2;             // Declare Box2 of type Box
   double volume = 0.0;  // Store the volume of a box here
   Box1.height = 18.0;   // Define the values
   Box1.length = 78.0;   // of the members of
   Box1.breadth = 24.0;  // the object Box1
   Box2.height = Box1.height - 10;  // Define Box2
   Box2.length = Box1.length/2.0;   // members in
   Box2.breadth = 0.25*Box1.length; // terms of Box1

   volume = Box1.Volume();          // Calculate volume of Box1
   cout << endl
        << "Volume of Box1 = " << volume;
   cout << endl
        << "Volume of Box2 = "
        << Box2.Volume();

   cout << endl
        << "A Box object occupies "
        << sizeof Box1 << " bytes.";
   cout << endl;

   return 0;
}

How It Works

The new code that we've added to the class definition is highlighted. It's just the definition of the function Volume(), which is a member function of the class. It also has the same access attribute as the data members: public. This is because every class member declared following an access attribute will have that access attribute, until another one is specified within the class definition. The function Volume() returns the volume of a Box object as a value of type double. The expression in the return statement is just the product of the three data members of the class.

There's no need to qualify the names of the class members in any way when accessing them in member functions. The unqualified member names automatically refer to the members of the object that is current when the member function is executed.

The member function Volume() is used in the highlighted statements in main(), after initializing the data members (as in the first example). Using the same name for a variable in main() causes no conflict or problem. You can call a member function of a particular object by writing the name of the object to be processed, followed by a period, followed by the member function name. As we noted above, the function will automatically access the data members of the object for which it was called, so the first use of Volume() calculates the volume of Box1. Using only the name of a data member will always refer to the member of the object for which the member function has been called.

The member function is used a second time directly in the output statement to produce the volume of Box2. If you execute this example, it will produce this output:

Note that the Box object is still the same number of bytes. Adding a function member to a class doesn't affect the size of the objects. Obviously, a member function has to be stored in memory somewhere, but there's only one copy regardless of how many class objects have been declared, and it isn't counted when the operator sizeof produces the number of bytes that an object occupies.

The names of the class data members in the member function automatically refer to the data members of the specific object used to call the function, and the function can only be called for a particular object of the class. In this case, this is done by using the direct member access operator (.) with the name of an object.

If you try to call a member function without specifying an object name, your program will not compile.

Positioning a Member Function Definition

A member function definition need not be placed inside the class definition. If you want to put it outside the class definition, you need to put the prototype for the function inside the class. If we rewrite the previous class with the function definition outside, then the class definition looks like this:

class Box               // Class definition at global scope
{
public:
   double length;       // Length of a box in inches
   double breadth;      // Breadth of a box in inches
   double height;       // Height of a box in inches
   double Volume(void); // Member function prototype
};

Now we need to write the function definition, but there has to be some way of telling the compiler that the function belongs to the class Box. This is done by prefixing the function name with the name of the class and separating the two with the scope resolution operator, ::, which is formed from two successive colons. The function definition would now look like this:

   // Function to calculate the volume of a box
   double Box::Volume(void)
   {
      return length * breadth * height;
   }

It will produce the same output as the last example. However, it isn't exactly the same program. In the second case, all calls to the function are treated in the way that we're already familiar with. However, when we defined the function within the definition of the class in Ex6_02.cpp, the compiler implicitly treated the function as an inline function.

Inline Functions

With an inline function, the compiler tries to expand the code in the body of the function in place of a call to the function. This avoids much of the overhead of calling the function and, therefore, speeds up your code. This is illustrated here:

Of course, the compiler takes care of ensuring that expanding a function inline doesn't cause any problems with variable names or scope.

The compiler may not always be able to insert the code for a function inline (such as with recursive functions or functions for which you have obtained an address), but generally it will work. It's best used for very short, simple functions, such as our function Volume(), because they will execute faster and will not significantly increase the size of the executable module.

With the function definition outside of the class definition, the compiler treats the function as a normal function and a call of the function will work in the usual way. However, it's also possible to tell the compiler that, if possible, we would like the function to be considered as inline. This is done by simply placing the keyword inline at the beginning of the function header. So, for our function, the definition would be as follows:

   // Function to calculate the volume of a box
   inline double Box::Volume(void)
   {
      return length * breadth * height;
   }

With this definition for the function, the program would be exactly the same as the original. This allows you to put the member function definitions outside of the class definition, if you so choose, and still retain the speed benefits of inlining.

You can apply the keyword inline to ordinary functions in your program that have nothing to do with classes and get the same effect. However, remember that it's best used for short, simple functions.

We now need to understand a little more about what happens when we declare an object of a class.


(Class Constructors)

Class Constructors

In the program above, we declared our Box objects, Box1 and Box2, and then laboriously worked through each of the data members for each object, in order to assign an initial value to it. This is unsatisfactory from several points of view. First of all, it would be easy to overlook initializing a data member, particularly in the case of a class which had many more data members than our Box class. Initializing the data members of several objects of a complex class could involve pages of assignment statements. The final constraint on this approach arises when we get to defining data members of a class that don't have the attribute public - we won't be able to access them from outside the class anyway. There has to be a better way, and of course there is - it's known as the class constructor.

What is a Constructor?

A class constructor is a special function in a class that is called when a new object of the class is declared. It therefore provides the opportunity to initialize objects as they are created and to ensure that data members only contain valid values.

You have no leeway in naming a class constructor - it always has the same name as the class in which it is defined. The function Box(), for example, is a constructor for our Box class. It also has no return type. It's wrong to specify a return type for a constructor; you must not even write it as void. The primary function of a class constructor is to assign initial values to the data elements of the class, and no return type is necessary or, indeed, permitted.

Try It Out - Adding a Constructor to the Box class

Let's extend our Box class to incorporate a constructor.

// Ex6_03.cpp
// Using a constructor
#include <iostream>
using namespace std;

class Box                 // Class definition at global scope
{
   public:
      double length;      // Length of a box in inches
      double breadth;     // Breadth of a box in inches
      double height;      // Height of a box in inches

      // Constructor definition
      Box(double lv, double bv, double hv)
      {
         cout << endl << "Constructor called.";
         length = lv;                       // Set values of
         breadth = bv;                      // data members
         height = hv;
      }

      // Function to calculate the volume of a box
      double Volume()
      {
         return length * breadth * height;
      }
};

int main(void)
{
   Box Box1(78.0,24.0,18.0);   // Declare and initialize Box1
   Box CigarBox(8.0,5.0,1.0);  // Declare and initialize CigarBox
   double volume = 0.0;        // Store the volume of a box here
   volume = Box1.Volume();     // Calculate volume of Box1

   cout << endl
        << "Volume of Box1 = " << volume;
   cout << endl
        << "Volume of CigarBox = "
        << CigarBox.Volume();
   cout << endl;

   return 0;
}

How It Works

The constructor, Box(), has been written with three parameters of type double, corresponding to the initial values for the length, breadth and height members of a Box object. The first statement in the constructor outputs a message so that we can tell when it's been called. You wouldn't do this in production programs, but, since it's very helpful in showing when a constructor is called, it's often used when testing a program. We'll use it regularly for the purposes of illustration. The code in the body of the constructor is very simple. It just assigns the arguments passed to the corresponding data members. If necessary, we could also include checks that valid, non-negative arguments are supplied and, in a real context, you probably would want to do this, but our primary interest here is in seeing how the mechanism works.

Within main(), we declare the object Box1 with initializing values for the data members length, breadth, and height, in sequence. These are in parentheses following the object name. This uses the functional notation for initialization, which can also be applied to initializing ordinary variables of basic types. We also declare a second object of type Box, called CigarBox, which also has initializing values.

The volume of Box1 is calculated using the member function Volume() as in the previous example and is then displayed on the screen. We also display the value of the volume of CigarBox. The output from the example is:

The first two lines are output from the two calls of the constructor, Box(), once for each object declared. The constructor that we've supplied in the class definition is automatically called when a Box object is declared, so both Box objects are initialized with the initializing values appearing in the declaration. These are passed to the constructor as arguments, in the sequence that they are written in the declaration. As you can see, the volume of Box1 is the same as before and CigarBox has a volume looking suspiciously like the product of its dimensions, which is quite a relief.

The Default Constructor

Try modifying the last example by adding the declaration for Box2 that we had previously:

Box Box2;       // Declare Box2 of type Box

Here, we've left Box2 without initializing values. When you rebuild this version of the program, you'll get the error message:

error C2512: 'Box': no appropriate default constructor available

This means that the compiler is looking for a default constructor for Box2, either one that needs no arguments, because none are specified in the constructor definition, or one whose arguments are all optional, because we haven't supplied any initializing values for the data members. Well, this statement was perfectly satisfactory in Ex6_02.cpp, so why doesn't it work now?

The answer is that the previous example used a default constructor that was supplied by the compiler, because we didn't supply one. Since in this example we did supply a constructor, the compiler assumed that we were taking care of everything and didn't supply the default. So, if you still want to use declarations for Box objects which aren't initialized, you have to include the default constructor yourself. What exactly does the default constructor look like? In the simplest case, it's just a constructor that accepts no arguments, it doesn't even need to do anything:

Box()    // Default constructor
{}       // Totally devoid of statements

Try It Out - Supplying a Default Constructor

Let's add our version of the default constructor to the last example, along with the declaration for Box2, plus the original assignments for the data members of Box2. We must enlarge the default constructor just enough to show that it is called. Here is the next version of the program:

// Ex6_04.cpp
// Supplying and using a default constructor
#include <iostream >
using namespace std;

class Box                // Class definition at global scope
{
   public:
      double length;     // Length of a box in inches
      double breadth;    // Breadth of a box in inches
      double height;     // Height of a box in inches

      // Constructor definition
      Box(double lv, double bv, double hv)
      {
         cout << endl << "Constructor called.";
         length = lv;                   // Set values of
         breadth = bv;                  // data members
         height = hv;
      }

      // Default constructor definition
      Box()
      { cout << endl << "Default constructor called."; }

      // Function to calculate the volume of a box
      double Volume()
      {
         return length * breadth * height;
      }
};

int main(void)
{
   Box Box1(78.0,24.0,18.0);  // Declare and initialize Box1
   Box Box2;                  // Declare Box2 - no initial values
   Box CigarBox(8.0,5.0,1.0); // Declare and initialize CigarBox
   double volume = 0.0;       // Store the volume of a box here
   volume = Box1.Volume();    // Calculate volume of Box1

   cout << endl
        << "Volume of Box1 = " << volume;

   Box2.height = Box1.height - 10;     // Define Box2
   Box2.length = Box1.length/2.0;      // members in
   Box2.breadth = 0.25*Box1.length;    // terms of Box1

   cout << endl
        << "Volume of Box2 = "
        << Box2.Volume();

   cout << endl
        << "Volume of CigarBox = "
        << CigarBox.Volume();
   cout << endl;

   return 0;
}

How It Works

Now that we have included our own version of the default constructor, there are no error messages from the compiler and everything works. The program produces this output:

All that our default constructor does is to display a message. Evidently, it was called when we declared the object Box2. We also get the correct value for the volumes of all three Box objects, so the rest of the program is working as it should.

One aspect of this example that you may have noticed is that we now know we can overload constructors just as we overloaded functions. We've just run an example with two constructors that differ only in their parameter list. One has three parameters of type double and the other has no parameters at all.

Assigning Default Values in a Constructor

When we discussed functions in C++, we saw how we could specify default values for the parameters to a function in the function prototype. We can also do this for class member functions, including constructors. If we put the definition of the member function inside the class definition, we can put the default values for the parameters in the function header. If we only include the prototype of a function in the class definition, the default parameter value should go in the prototype.

If we decided that the default size for a Box object was a unit box with all sides of length 1, we could alter the class definition in the last example to this:

class Box               // Class definition at global scope
{
   public:
      double length;    // Length of a box in inches
      double breadth;   // Breadth of a box in inches
      double height;    // Height of a box in inches

      // Constructor definition
      Box(double lv = 1.0, double bv = 1.0, double hv = 1.0)
      {
         cout << endl << "Constructor called.";
         length = lv;                       // Set values of
         breadth = bv;                      // data members
         height = hv;
      }

      // Default constructor definition
      Box()
      { cout << endl << "Default constructor called."; }

      // Function to calculate the volume of a box
      double Volume()
      {
          return length * breadth * height;
      }
};

If we make this change to the last example, what happens? We get another error message from the compiler, of course. We get these useful comments:

warning C4520: 'Box': multiple default constructors specified

error C2668: 'Box::Box': ambiguous call to overloaded function

This means that the compiler can't work out which of the two constructors to call - the one for which we have set default values for the parameters or the constructor that doesn't accept any parameters. This is because the declaration of Box2 requires a constructor without parameters, and either constructor can now be called in this case. The immediately obvious solution to this is to get rid of the constructor that accepts no parameters. This is actually beneficial. Without this constructor, any Box object that is declared without being explicitly initialized will automatically have its members initialized to 1.

Try It Out - Supplying Default Values for Constructor Arguments

We can demonstrate this with the following simplified example:

// Ex6_05.cpp
// Supplying default values for constructor arguments
#include <iostream>
using namespace std;

class Box               // Class definition at global scope
{
   public:
      double length;    // Length of a box in inches
      double breadth;   // Breadth of a box in inches
      double height;    // Height of a box in inches

      // Constructor definition
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         cout << endl << "Constructor called.";
         length = lv;                       // Set values of
         breadth = bv;                      // data members
         height = hv;
      }

      // Function to calculate the volume of a box
      double Volume()
      {
          return length * breadth * height;
      }
};

int main(void)
{
   Box Box2;              // Declare Box2 - no initial values
   cout << endl
        << "Volume of Box2 = "
        << Box2.Volume();
   cout << endl;
   return 0;
}

How It Works

We only declare a single uninitialized Box variable, Box2, because that's all we need for demonstration purposes. This version of the program produces the following output:

This shows that the constructor with default parameter values is doing its job of setting the values of objects that have no initializing values specified.

You shouldn't assume from the above example that this is the only, or even the recommended, way of implementing the default constructor. There will be many occasions where you won't want to assign default values in this way, in which case you'll need to write a separate default constructor. There will even be times when you don't want to have a default constructor operating at all, even though you have defined another constructor. This would ensure that all declared objects of a class must have initializing values explicitly specified in their declaration.

Using an Initialization List in a Constructor

Previously, we initialized the members of an object in the class constructor using explicit assignment. We could also have used a different technique, using what is called an initialization list. We can demonstrate this with an alternative version of the constructor for the class Box:

   // Constructor definition using an initialization list
   Box(double lv=1.0, double bv=1.0, double hv=1.0): length(lv),
                                                     breadth(bv),
                                                     height(hv)
   {
      cout << endl << "Constructor called.";
   }

Now the values of the data members are not set in assignment statements in the body of the constructor. As in a declaration, they are specified as initializing values using functional notation and appear in the initializing list as part of the function header. The member length is initialized by the value of lv, for example. This can be rather more efficient than using assignments as we did in the previous version. If you substitute this version of the constructor in the previous example, you will see that it works just as well.

Note that the initializing list for the constructor is separated from the parameter list by a colon and that each of the initializers is separated by a comma. This technique for initializing parameters in a constructor is important, because, as we shall see later, it's the only way of setting values for certain types of data members of an object.


(Private Members of a Class)

Private Members of a Class

Having a constructor that sets the values of the data members of a class object, but still admits the possibility of any part of a program being able to mess with what are essentially the guts of an object, is almost a contradiction in terms. To draw an analogy, once you have arranged for a brilliant surgeon such as Dr. Kildare, whose skills were honed over years of training, to do things to your insides, letting the local plumber, bricklayer or the folks from Hill Street Blues have a go hardly seems appropriate. We need some protection for our class data members.

We can get it by using the keyword private when we define the class members. Class members which are private can, in general, only be accessed by member functions of a class. There's one exception, but we'll worry about that later. A normal function has no direct means of accessing the private members of a class. This is shown here:

Having the possibility of specifying class members as private also enables you to separate the interface to the class from its internal implementation. The interface to a class is composed of the public members and the public member functions in particular, since they can provide indirect access to all the members of a class, including the private members. By keeping the internals of a class private, you can later modify them to improve performance, for example, without necessitating modifications to the code that uses the class through its public interface. To keep them safe from unnecessary meddling, it's good practice to declare data and function members of a class that don't need to be exposed as private. Only make public what is essential to the use of your class.

Try It Out - Private Data Members

We can rewrite the Box class to make its data members private.

// Ex6_06.cpp
// A class with private members
#include <iostream>
using namespace std;
class Box            // Class definition at global scope
{
   public:
      // Constructor definition
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         cout << endl << "Constructor called.";
         length = lv;                       // Set values of
         breadth = bv;                      // data members
         height = hv;
      }

      // Function to calculate the volume of a box
      double Volume()
      {
          return length * breadth * height;
      }

   private:
      double length;   // Length of a box in inches
      double breadth;  // Breadth of a box in inches
      double height;   // Height of a box in inches
};
int main(void)
{
   Box Match(2.2, 1.1, 0.5); // Declare Match box
   Box Box2;                 // Declare Box2 - no initial values

   cout << endl
        << "Volume of Match = "
        << Match.Volume();

// Uncomment the following line to get an error
// Box2.length = 4.0;

   cout << endl
        << "Volume of Box2 = "
        << Box2.Volume();
   cout << endl;

   return 0;
}

How It Works

The definition of the class Box now has two sections. The first is the public section containing the constructor and the member function Volume(). The second section is specified as private and contains the data members. Now the data members can only be accessed by the member functions of the class. We don't have to modify any of the member functions - they can access all the data members of the class anyway. However, if you uncomment the statement in the function main(), assigning a value to the member length of the object Box2, you'll get a compiler error message confirming that the data member is inaccessible.

A point to remember is that using a constructor or a member function is now the only way to get a value into a private data member of an object. You have to make sure that all the ways in which you might want to set or modify private data members of a class are provided for through member functions.

We could also put functions into the private section of a class. In this case, they can only be called by other member functions. If you put the function Volume() in the private section, you will get a compiler error from the statements that attempt to use it in the function main(). If you put the constructor in the private section, you won't be able to declare any members of the class.

The above example generates this output:

This demonstrates that the class is still working satisfactorily, with its data members defined as having the access attribute private. The major difference is that they are now completely protected from unauthorized access and modification.

If you don't specify otherwise, the default access attribute which applies to members of a class is private. You could, therefore, put all your private members at the beginning of the class definition and let them default to private by omitting the keyword. However, it's better to take the trouble to explicitly state the access attribute in every case, so there can be no doubt about what you intend.

Of course, you don't have to make all your data members private. If the application for your class requires it, you can have some data members defined as private and some as public. It all depends on what you're trying to do. If there's no reason to make members of a class public, it is better to make them private as it makes the class more secure. Ordinary functions won't be able to access any of the private members of your class.

Accessing private Class Members

On reflection, declaring all the data members of a class as private might seem rather extreme. It's all very well protecting them from unauthorized modification, but that's no reason to keep their values a secret. What we need is a Freedom of Information Act for private members.

You don't need to start writing to your state senator to get it - it's already available to you. All you need to do is to write a member function to return the value of a data member. Look at this member function for the class Box:

inline double Box::GetLength(void)
{
   return length;
}

Just to show how it looks, this has been written as a member function definition which is external to the class. We've specified it as inline, since we'll benefit from the speed increase, without increasing the size of our code too much. Assuming that you have the declaration of the function in the public section of the class, you can use it by writing this statement:

len = Box2.GetLength();   // Obtain data member length

All you need to do is to write a similar function for each data member that you want to make available to the outside world, and their values can be accessed without prejudicing the security of the class. Of course, if you put the definitions for these functions within the class definition, they will be inline by default.

The friend Functions of a Class

There may be circumstances when, for one reason or another, you want certain selected functions which are not members of a class to be able, nonetheless, to access all the members of a class - a sort of elite group with special privileges. Such functions are called friend functions of a class and are defined using the keyword friend. You can either include the prototype of a friend function in the class definition, or you can include the whole function definition. Functions which are friends of a class and are defined within the class definition are also by default inline.

Friend functions are not members of the class, and therefore the access attributes do not apply to them. They are just ordinary global functions with special privileges.

Let's suppose that we wanted to implement a friend function in the Box class to compute the surface area of a Box object.

Try It Out - Using a friend to Calculate the Surface Area

We can see how this works in the following example:

// Ex6_07.cpp
// Creating a friend function of a class
#include <iostream>
using namespace std;

class Box            // Class definition at global scope
{
   public:
      // Constructor definition
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         cout << endl << "Constructor called.";
         length = lv;                       // Set values of
         breadth = bv;                      // data members
         height = hv;
      }

      // Function to calculate the volume of a box
      double Volume()
      {
         return length * breadth * height;
      }

   private:
      double length;    // Length of a box in inches
      double breadth;   // Breadth of a box in inches
      double height;    // Height of a box in inches

   //Friend function
   friend double BoxSurface(Box aBox);
};

// friend function to calculate the surface area of a Box object
double BoxSurface(Box aBox)
{
   return 2.0*(aBox.length*aBox.breadth +
               aBox.length*aBox.height +
               aBox.height*aBox.breadth);
}

int main(void)
{
   Box Match(2.2, 1.1, 0.5); // Declare Match box
   Box Box2;                 // Declare Box2 - no initial values

   cout << endl
        << "Volume of Match = "
        << Match.Volume();

   cout << endl
        << "Surface area of Match = "
        << BoxSurface(Match);

   cout << endl
        << "Volume of Box2 = "
        << Box2.Volume();

   cout << endl
        << "Surface area of Box2 = "
        << BoxSurface(Box2);
   cout << endl;

   return 0;
}

How It Works

We declare the function BoxSurface() as a friend of the Box class by writing the function prototype with the keyword friend at the front. Since the BoxSurface() function itself is a global function, it makes no difference where we put the friend declaration within the definition of the class, but it's a good idea to be consistent when you position this sort of declaration. You can see that we have chosen to position ours after all the public and private members of the class. Remember that a friend function isn't a member of the class, so access attributes don't apply.

The definition of the function follows that of the class. Note that we specify access to the data members of the object within the definition of BoxSurface(), using the Box object passed to the function as a parameter. Because a friend function isn't a class member, the data members can't be referenced just by their names. They each have to be qualified by the object name in exactly the same way as they might in an ordinary function, except, of course, that an ordinary function can't access the private members of a class. A friend function is the same as an ordinary function except that it can access all the members of a class without restriction.

The example produces this output:

This is exactly what you would expect. The friend function is computing the surface area of the Box objects from the values of the private members.

Placing friend Function Definitions Inside the Class

We could have combined the definition of the function with its declaration as a friend of the Box class within the class definition - the code would run as before. However, this has a number of disadvantages relating to the readability of the code. Although the function would still have global scope, this wouldn't be obvious to readers of the code, since the function would be hidden in the body of the class definition, and particularly since the function would no longer show up in the ClassView.

The Default Copy Constructor

Suppose we declare and initialize a Box object Box1 with this statement:

Box Box1(78.0, 24.0, 18.0);

We now want to create another Box object, identical to the first. We would like to initialize the second Box object with Box1.

Try It Out - Copying Information Between Instances

We can try this out with the main() function which follows:

// Ex6_08.cpp
// Initializing an object with an object of the same class
#include <iostream>
using namespace std;

class Box       // Class definition at global scope
{
   public:
      // Constructor definition
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         cout << endl << "Constructor called.";
         length = lv;                       // Set values of
         breadth = bv;                      // data members
         height = hv;
      }

      // Function to calculate the volume of a box
      double Volume()
      {
         return length * breadth * height;
      }

   private:
      double length;   // Length of a box in inches
      double breadth;  // Breadth of a box in inches
      double height;   // Height of a box in inches
};

int main(void)
{
   Box Box1(78.0, 24.0, 18.0);
   Box Box2 = Box1;           // Initialize Box2 with Box1

   cout << endl
        << "Box1 volume = " << Box1.Volume()
        << endl
        << "Box2 volume = " << Box2.Volume();

   cout << endl;
   return 0;
}

How It Works

This example will produce this:

Clearly, the program is working as we would want, with both boxes having the same volume. However, as you can see from the output, our constructor was called only once for the creation of Box1. But how was Box2 created? The mechanism is similar to the one that we experienced when we had no constructor defined and the compiler supplied a default constructor to allow an object to be created. In this case, the compiler generates a default version of what is referred to as a copy constructor.

A copy constructor does exactly what we're doing here - it creates an object of a class by initializing it with an existing object of the same class. The default version of the copy constructor creates the new object by copying the existing object, member by member.

This is fine for simple classes such as Box, but for many classes - classes that have pointers or arrays as members for example - it won't work properly. Indeed, with such classes it can create serious errors in your program. In these cases, you need to create your own class copy constructor. This requires a special approach, which we'll look into more fully towards the end of this chapter, and again in the next chapter.


(The this Pointer)

The this Pointer

In our Box class, we wrote the function Volume() in terms of the class member names in the definition of the class. Of course, every object of type Box that we create contains these members, so there has to be a mechanism for the function to refer to the members of the particular object for which the function is called.

When any member function executes, it automatically contains a hidden pointer with the name this, which points to the object used with the function call. Therefore, when the member length is accessed in the function Volume() during execution, it's actually referring to this->length, which is the fully-specified reference to the object member that is being used. The compiler takes care of adding the necessary pointer name this to the member names in the function.

If you need to, you can use the pointer this explicitly within a member function. You might, for example, want to return a pointer to the current object.

Try It Out - Explicit Use of this

We could add a public function to our class Box to compare the volume of two Box objects.

// Ex6_09.cpp
// Using the pointer this
#include <iostream>
using namespace std;

class Box            // Class definition at global scope
{
   public:
      // Constructor definition
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         cout << endl << "Constructor called.";
         length = lv;                        // Set values of
         breadth = bv;                       // data members
         height = hv;
      }

      // Function to calculate the volume of a box
      double Volume()
      {
         return length * breadth * height;
      }

      // Function to compare two boxes which
      // returns true if the first is greater
      // than the second, and false otherwise
      int compare(Box xBox)
      {
         return this->Volume() > xBox.Volume();
      }

   private:
      double length;     // Length of a box in inches
      double breadth;    // Breadth of a box in inches
      double height;     // Height of a box in inches
};

int main(void)
{
   Box Match(2.2, 1.1, 0.5);    // Declare Match box
   Box Cigar(8.0, 5.0,1.0);    // Declare Cigar box

   if(Cigar.compare(Match))
      cout << endl
           << "Match is smaller than Cigar";
   else
      cout << endl
           << "Match is equal to or larger than Cigar";

   cout << endl;
   return 0;
}

How It Works

The member function compare() returns true if the prefixed Box object in the function call has a greater volume than the Box object specified as an argument, and false if it doesn't. In the return statements, the prefixed object is referred to through the pointer this, used with the indirect member access operator, ->, that we learned about earlier.

Remember that you use the direct member access operator . when dealing with objects and the indirect member access operator -> when dealing with pointers to objects. this is a pointer.

Using the pointer this in this example demonstrates that it exists and does work, but it's quite unnecessary to use it explicitly in this particular case. If you change the return statement in the compare() function to be,

return Volume() > xBox.Volume();

you'll find that it works just as well. Any references to unadorned member names are automatically assumed to be the members of the object pointed to by this.

The compare() function is used in main() to check the relationship between the volumes of the objects Match and Cigar. The output from the program is:

This confirms that the Cigar object is larger than the Match object.

It also wasn't essential to write the compare() function as a class member. We could just as well have written it as an ordinary function with the objects as arguments. Note that this isn't true of the function Volume(), since it needs to access the private data members of the class. Of course, if the function compare() was implemented as an ordinary function, it wouldn't have the pointer this, but it would still be very simple:

// Comparing two Box objects - ordinary function version
int compare(Box B1, Box B2)
{
   return B1.Volume() > B2.Volume();
}

This has both objects as arguments and returns true if the volume of the first is greater than the last. You would use this function to perform the same function as in the last example with this statement:

if(compare(Cigar, Match))
      cout << endl
           << "Match is smaller than Cigar";
   else
      cout << endl
           << "Match is equal to or larger than Cigar";

If anything, this looks slightly better and easier to read than the original version. However, there's a much better way to do this, which we shall see before the end of the chapter.


(Arrays of Objects of a Class)

Arrays of Objects of a Class

We can declare an array of objects of a class in exactly the same way that we have declared an ordinary array, where the elements were one of the built-in types. Each element of an array of class objects causes the default constructor to be called.

Try It Out - Arrays of Class Objects

We can use the class definition of Box from the last example, but modified to include a specific default constructor:

// Ex6_10.cpp
// Using an array of class objects
#include <iostream>
using namespace std;

class Box               // Class definition at global scope
{
   public:
      // Constructor definition
      Box(double lv, double bv=1.0, double hv=1.0)
      {
         cout << endl << "Constructor called.";
         length = lv;                        // Set values of
         breadth = bv;                       // data members
         height = hv;
      }

      Box()                          // Default constructor
      {
         cout << endl
              << "Default constructor called.";
         length = breadth = height = 1.0;
      }

      // Function to calculate the volume of a box
      double Volume()
      {
         return length * breadth * height;
      }

   private:
      double length;               // Length of a box in inches
      double breadth;              // Breadth of a box in inches
      double height;               // Height of a box in inches
};

int main(void)
{
   Box Boxes[5];              // Array of Box objects declared
   Box Cigar(8.0, 5.0,1.0);   // Declare Cigar box

   cout << endl
        << "Volume of Boxes[3] = " << Boxes[3].Volume()
        << endl
        << "Volume of Cigar = " << Cigar.Volume();
   cout << endl;

   return 0;
}

How It Works

We have modified the constructor accepting arguments so that only two default values are supplied, and we have added a default constructor which initializes the data members to 1 after displaying a message that it was called. We will now be able to see which constructor was called when. The constructors now have quite distinct parameter lists, so there's no possibility of the compiler confusing them. The program produces this:

We can see that the default constructor was called five times, once for each element of the array Boxes. The other constructor was called to create the object Cigar. It's clear from the output that the default constructor initialization is working satisfactorily, as the volume of the forth array element is 1.


(Static Members of a Class)

Static Members of a Class

Both data members and function members of a class can be declared as static. Because the context is a class definition, there's a little more to it than the effect of the keyword static outside of a class, so let's look at static data members.

Static Data Members of a Class

When we declare data members of a class as static, they are defined only once and are shared between all objects of the class. Each object gets its own copies of each of the ordinary data members of a class, but only one instance of each static data member exists, regardless of how many class objects have been defined.

One use for a static data member is to count how many objects actually exist. We could add a static data member to the public section of the Box class by adding the following statement to the previous class definition:

static int ObjectCount;     // Count of objects in existence

We now have a problem. How do we initialize the static data member? We can't put it in the class definition - that's simply a blueprint for an object, and initializing values are not allowed. We don't want to initialize it in a constructor, because we want to increment it every time the constructor is called. We can't initialize it in another member function, since a member function is associated with an object and we want it initialized before any object is created. The answer is to write the initialization outside of the class definition with this statement:

// Initialize static member of class Box
int Box::ObjectCount = 0;

Notice that the keyword static is not included here. However, we do need to qualify the member name by using the class name and the scope resolution operator so that the compiler understands that we're referring to a static member of the class. Otherwise, we would simply create a global variable that was nothing to do with the class.

Try It Out - Counting Instances

Let's add the static data member and the object counting capability to the last example.

// Ex6_11.cpp
// Using a static data member in a class
#include <iostream>
using namespace std;

class Box           // Class definition at global scope
{
   public:
      static int ObjectCount; // Count of objects in existence

      // Constructor definition
      Box(double lv, double bv=1.0, double hv=1.0)
      {
         cout << endl << "Constructor called.";
         length = lv;            // Set values of
         breadth = bv;           // data members
         height = hv;
         ObjectCount++;
      }

      Box()                      // Default constructor
      {
         cout << endl
              << "Default constructor called.";
         length = breadth = height = 1.0;
         ObjectCount++;
      }

      // Function to calculate the volume of a box
      double Volume()
      {
         return length * breadth * height;
      }

   private:
      double length;             // Length of a box in inches
      double breadth;            // Breadth of a box in inches
      double height;             // Height of a box in inches
};

// Initialize static member of class Box
int Box::ObjectCount = 0;

int main(void)
{
   Box Boxes[5];            // Array of Box objects declared
   Box Cigar(8.0, 5.0,1.0); // Declare Cigar box

   cout << endl << endl
        << "Number of objects (through class) = "
        << Box::ObjectCount;

   cout << endl
        << "Number of objects (through object) = "
        << Boxes[2].ObjectCount;

   cout << endl;
   return 0;
}

How It Works

This example will produce the output:

This code shows that it doesn't matter how we refer to the static member ObjectCount (whether through the class itself or any of the objects of that class). The value is the same and it is equal to the number of objects of that class that have been created. The six objects are obviously the five elements of the Boxes array, plus the object Cigar. It's interesting to note that static members of a class exist even though there may be no members of the class in existence. This is evidently the case, since we initialized the static member ObjectCount before any class objects were declared.

You must initialize a static data member, or the compiler will complain. The declaration in the class definition doesn't define a static variable so, until you define its initial value, it doesn't exist.

Static Function Members of a Class

By declaring a function member as static, you make it independent of any particular object of the class. Referencing members of the class from within a static function must be done using qualified names (as you would do with an ordinary global function accessing a public data member). The static member function has the advantage that it exists and can be called even if no objects of the class exist. In this case, only static data members can be used, since they are the only ones that exist. Thus, you can call a static function member of a class to examine static data members, even when you do not know for certain that any objects of the class exist. You could, therefore, use a static member function to determine whether some objects of the class have been created or, indeed, how many have been created.

Of course, once the objects have been defined, a static member function can access private as well as public members of class objects. A static function might have this prototype:

static void afunction(int n);

A static function can be called in relation to a particular object by a statement such as the following,

aBox.afunction(10);

where aBox is an object of the class. The same function could also be called without reference to an object. In this case, the statement would take the following form,

Box::afunction(10);

where Box is the class name. Using the class name and the scope resolution operator serves to tell the compiler to which class the function afunction() belongs.


(Pointers and References to Class Objects)

Pointers and References to Class Objects

Using pointers, and particularly references to class objects, is very important to object-oriented programming. Class objects can involve considerable amounts of data, so using a pass-by-value mechanism by specifying objects as parameters can be very time consuming and inefficient. There are also some techniques involving the use of references which are essential to some operations with classes. As we shall see, you can't write a copy constructor without using a reference parameter.

Pointers to Class Objects

You declare a pointer to a class object in the same way that you declare other pointers. For example, a pointer to objects of the Box class is declared in this statement:

Box* pBox = 0;     // Declare a pointer to Box

You can now use this to store the address of a Box object in an assignment in the usual way, using the address operator:

pBox = &Cigar;     // Store address of Box object Cigar in pBox

As we saw when we used the this pointer in the definition of the member function compare(), you can call a function using a pointer to an object. We can call the function Volume() for the pointer pBox in this statement:

// Display volume of object pointed to by pBox
cout << pBox->Volume();

Again, this uses the indirect member access operator. This is the typical notation used by most programmers for this kind of operation, so from now on we'll use it universally.

Try It Out - Pointers to Classes

Let's try exercising the indirect member access operator a little more. We will use the example Ex6_09.cpp as a base, but change it a little.

// Ex6_12.cpp
// Exercising the indirect member access operator
#include <iostream>
using namespace std;

class Box                 // Class definition at global scope
{
   public:
      // Constructor definition
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         cout << endl << "Constructor called.";
         length = lv;                        // Set values of
         breadth = bv;                       // data members
         height = hv;
      }

      // Function to calculate the volume of a box
      double Volume()
      {
         return length * breadth * height;
      }

      // Function to compare two boxes which
      // returns true if the first is greater
      // than the second, and false otherwise
      int compare(Box* pBox)
      {
         return this->Volume() > pBox->Volume();
      }

   private:
      double length;            // Length of a box in inches
      double breadth;           // Breadth of a box in inches
      double height;            // Height of a box in inches
};

int main(void)
{
   Box Boxes[5];               // Array of Box objects declared
   Box Match(2.2, 1.1, 0.5);   // Declare Match box
   Box Cigar(8.0, 5.0,1.0);    // Declare Cigar Box

   // Initialize pointer to Cigar object address
   Box* pB1 = &Cigar;
   Box* pB2 = 0;         // Pointer to Box initialized to null

   cout << endl
        << "Address of Cigar is " << pB1    // Display address
        << endl
        << "Volume of Cigar is " << pB1->Volume(); // Volume of
                                            // object pointed to

   pB2 = &Match;
   if(pB2->compare(pB1))                  // Compare via pointers
      cout << endl
           << "Match is greater than Cigar";
   else
      cout << endl
           << "Match is less than or equal to Cigar";

   pB1 = Boxes;                    // Set to address of array
   Boxes[2] = Match;               // Set 3rd element to Match
   cout << endl                    // Now access thru pointer
        << "Volume of Boxes[2] is " << (pB1 + 2)->Volume();

   cout << endl;
   return 0;
}

How It Works

The change to the class definition is not one of great substance. We've only modified the compare() function to accept a pointer to a Box object as an argument. The function main() merely exercises pointers to Box type objects in various, rather arbitrary, ways.

Within the function main(), after declaring an array Boxes, and the Box objects Cigar and Match, we declare two pointers to Box objects. The first, pB1, is initialized with the address of the object Cigar, and the second, pB2, is initialized to NULL. All of this uses the pointer in exactly the same way you would when you're applying a pointer to a basic type. The fact that we're using a pointer to a type that we have defined ourselves makes no difference.

We use pB1 with the indirect member access operator to generate the volume of the object pointed to, and the result is displayed. We then assign the address of Match to pB2 and use both pointers in calling the compare function. Because the argument of the function compare() is a pointer to a Box object, the function uses the indirect member access operator in calling the Volume() function for the object.

To demonstrate that we can use address arithmetic on the pointer pB1 when using it to select the member function, we set pB1 to the address of the first element of the array of type Box, Boxes. In this case, we select the third element of the array and calculate its volume. This is the same as the volume of Match.

If you run the example, the output window will look something like that shown below:

Of course, the value of the address for the Cigar object may well be different on your PC.

You can see that there were seven calls of the constructor for Box objects: five due to the array Boxes, plus one each for the objects Cigar and Match.

Overall, there's virtually no difference between using a pointer to a class object and using a pointer to a basic type, such as double.

References to Class Objects

References really come into their own when they are used with classes. As with pointers, there is virtually no difference between the way you declare and use references to class objects and the way in which we've already declared and used references to variables of basic types. To declare a reference to the object Cigar, for instance, we would write this:

Box& rCigar = Cigar;    // Define reference to object Cigar

To use a reference to calculate the volume of the object Cigar, you would just use the reference name where the object name would otherwise appear:

// Output volume of Cigar thru a reference
cout << rCigar.Volume();

As you may remember, a reference acts as an alias for the object it refers to, so the usage is exactly the same as using the original object name.

Implementing a Copy Constructor

The importance of references is really in the context of arguments and return values in functions, particularly class member functions. Let's return to the question of the copy constructor as a first toe in the water. For the moment, we'll sidestep the question of when you need to write your own copy constructor and concentrate on the problem of how you can write one. We'll use the class Box just to make the discussion more concrete.

The copy constructor is a constructor which creates an object by initializing it with an object of the same class, which has been created previously. It therefore needs to accept such an object as an argument. We might consider writing the prototype like this:

Box(Box initB);

Let's now consider what happens when this constructor is called. If we write this declaration,

Box myBox = Cigar;

this will generate a call of the copy constructor as follows:

Box::Box(Cigar);

This seems to be no problem, until you realize that the argument is passed by value. So, before the object Cigar can be passed, the compiler needs to arrange to make a copy of it. Therefore, it calls the copy constructor to make a copy of the argument for the call of the copy constructor. Unfortunately, since it is passed by value, this call also needs a copy of its argument to be made, so the copy constructor is called... and so on and so on. We end up with an infinite number of calls to the copy constructor.

The solution, as I'm sure you have guessed, is to use a const reference parameter. We could write the prototype of the copy constructor like this:

Box(const Box& initB);

Now the argument to the copy constructor doesn't need to be copied. It will be used to initialize the reference parameter, so no copying takes place. As you will remember from our discussion on references, if a parameter to a function is a reference, no copying of the argument occurs when the function is called. The function accesses the argument variable in the caller function directly. The const qualifier ensures that the argument can't be modified in the function.

This is an important use of the qualifier const. You should always declare a parameter of a function as const unless the function is intended to modify it.

We could implement the copy constructor as follows:

Box::Box(const Box& initB)
{
   length = initB.length;
   breadth = initB.breadth;
   height = initB.height;
}

This definition of the copy constructor assumes that it appears outside of the class definition. The name is therefore qualified with the class name using the scope resolution operator. Each data member of the object being created is initialized with the corresponding member of the object passed as an argument. Of course, we could equally well use the initialization list to set the values of the object.

This case is not an example of when we need to write a copy constructor. As we have seen, the default copy constructor works perfectly well with Box objects. We will get to why and when we need to write our own copy constructor in the next chapter.


(Summary)

Summary

You should now understand the basic ideas behind classes in C++. The key points to keep in mind from this chapter are:

  • A class provides a means of defining your own data types. They can reflect whatever types of objects your particular problem requires.

  • A class can contain data members and function members. The function members of a class always have free access to the data members of the same class.

  • Objects of a class are created and initialized using functions called constructors. These are automatically called when an object declaration is encountered. Constructors may be overloaded to provide different ways of initializing an object.

  • Members of a class can be specified as public, in which case they are freely accessible by any function in a program. Alternatively, they may be specified as private, in which case they may only be accessed by member functions or friend functions of the class.

  • Members of a class can be defined as static. Only one instance of each static member of a class exists, which is shared amongst all instances of the class, no matter how many objects of the class are created.

  • Every non-static member of a class contains the pointer this, which points to the current object for which the function was called.

  • Using references to class objects as arguments to function calls can avoid substantial overheads in passing complex objects to a function.

  • A copy constructor, which is a constructor for an object that is initialized with an existing object of the same class, must have its parameter specified as a const reference.


(More on Classes)

Chapter 7 - More on Classes

In this chapter, you will extend your knowledge of classes by understanding how to make your class objects work more like the basic types in C++. You will learn:

  • What a class destructor is and when and why it is necessary

  • How to implement a class destructor

  • How to allocate data members of a class in the free store and how to delete them when they are no longer required

  • When you must write a copy constructor for a class

  • What a union is and how it can be used

  • What a struct is

  • How to make objects of your class work with C++ operators such as + or *

  • What class templates are and how you define and use them

  • How to use classes in a practical example


(Class Destructors)

Class Destructors

Although this section heading refers to destructors, it's also about dynamic memory allocation. Allocating memory in the free store for class members can only be managed with the aid of a destructor, in addition to a constructor of course, and, as you'll see, using dynamically allocated class members will require you to write your own copy constructor.

What is a Destructor?

A destructor is a function that destroys an object when it is no longer required or when it goes out of scope. It's called automatically when an object goes out of scope. Destroying an object involves freeing the memory occupied by the data members of the object (except for static members, which continue to exist even when there are no class objects in existence). The destructor for a class is a member function with the same name as the class, preceded by a tilde ~. The class destructor doesn't return a value and doesn't have parameters defined. For the class Box, the prototype of the class destructor is:

~Box();            // Class destructor prototype

It's an error to specify a return value or parameters for a destructor.

The Default Destructor

All the objects that we have been using up to now have been destroyed automatically by the default destructor for the class. This is generated by the compiler in the absence of any explicit destructor being provided with a class. The default destructor doesn't delete objects or object members that have been allocated in the free store by the operator new. You must explicitly use the delete operator to destroy objects that have been created using the operator new, just as you would with ordinary variables. If you decide to allocate memory for members of an object dynamically, you must use the operator delete to implement a class destructor which frees any memory that was allocated by the operator new.

Try It Out - A Simple Destructor

We need some practice in writing our own destructor. First, to show when the destructor is called, we can include a destructor in the class Box. The class definition in this example is based on the last example in the previous chapter, Ex6_12.cpp.

// Ex7_01.cpp
// Class with an explicit destructor
#include <iostream>
using namespace std;

class Box              // Class definition at global scope
{
   public:
      // Destructor definition
      ~Box()
      { cout << "Destructor called." << endl; }

      // Constructor definition
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         cout << endl << "Constructor called.";
         length = lv;  // Set values of
         breadth = bv; // data members
         height = hv;
      }

      // Function to calculate the volume of a box
      double Volume()
      {
         return length * breadth * height;
      }

      // Function to compare two boxes which
      // returns true if the first is greater
      // than the second, and false otherwise
      int compare(Box* pBox)
      {
         return this->Volume() > pBox->Volume();
      }

   private:
      double length;    // Length of a box in inches
      double breadth;   // Breadth of a box in inches
      double height;    // Height of a box in inches
};

int main()
{
   Box Boxes[5];             // Array of Box objects declared
   Box Cigar(8.0, 5.0, 1.0); // Declare Cigar box
   Box Match(2.2, 1.1, 0.5); // Declare Match box

   // Initialize pointer to Cigar object address
   Box* pB1 = &Cigar;
   Box* pB2 = 0;            // Pointer to Box initialized to null

   cout << endl
        << "Volume of Cigar is "
        << pB1->Volume();    // Volume of obj. pointed to

   pB2 = Boxes;                     // Set to address of array
   Boxes[2] = Match;                // Set 3rd element to Match

   cout << endl                     // Now access thru pointer
        << "Volume of Boxes[2] is " << (pB2 + 2)->Volume();

   cout << endl;
   return 0;
}

How It Works

The only thing that our destructor does is to display a message showing that it was called. The output is:

We get one call of the destructor at the end of the program for each of the objects that exist. For each constructor call that occurred, there's a matching destructor call. We don't need to call the destructor explicitly - when an object of a class goes out of scope, the compiler will arrange for the destructor for the class to be called automatically. In our example, the destructors are called after main() has finished executing, so if there's an error in a destructor, it's quite possible for a program to crash after main() has safely terminated.

Destructors and Dynamic Memory Allocation

You will find that you often want to allocate memory for class data members dynamically. We can use the operator new in a constructor to allocate space in memory for an object member. In such a case, we must assume responsibility for deleting this space by providing a suitable destructor. Let's first define a simple class where we can do this.

Suppose we want a class where each object is a message of some description, for example, a text string. We want the class to be as memory efficient as possible, so, rather than defining a data member as a char array big enough to hold the maximum length string that we might require, we'll allocate memory on the free store for a message when an object is created. Here's the class definition:

//Listing 02_01
class Message
{
   private:
      char* pmessage;    // Pointer to object text string

   public:
      // Function to display a message
      void ShowIt(void)
      {
         cout << endl << pmessage;
      }

      // Constructor definition
      Message(const char* text = "Default message")
      {
         // Allocate space for text
         pmessage = new char[strlen(text)+1];
         // Copy text to new memory
         strcpy(pmessage, text);
      }

      ~Message();        // Destructor prototype
};

This class has only one data member defined, pmessage, which is a pointer to a text string. This is defined in the private section of the class, so that it can't be accessed from outside the class.

In the public section, we have a function ShowIt(), which will output a Message object to the screen. We also have the definition of a constructor and we have the prototype for the class destructor, ~Message(), which we'll come to in a moment.

The constructor for the class requires a string as an argument, but if none is passed, it uses the default value specified. The constructor obtains the length of the string supplied as an argument, excluding the terminating NULL, using the function strlen(). For the constructor to use this library function, there must be a #include statement for the header file string. By adding 1 to the value that the function strlen() returns, the constructor defines the number of bytes of memory necessary to store the string in the free store.

We're assuming that we have our own function to handle out-of-memory conditions, so we don't bother to test the pointer returned for NULL. (See Chapter 5 for information on handling out-of-memory conditions.)

Having obtained the memory for the string using the operator new, we use the strcpy() function, which is also declared in the header file string, to copy the string supplied as an argument into the memory allocated for it. This function copies the string specified by the second pointer argument to the address contained in the first pointer argument.

We now need to write a class destructor that will free up the memory allocated for a message. If we don't provide this, there's no way to delete the memory allocated for an object. If we use this class in a program, where a large number of Message objects are created, the free store will be gradually eaten away until the program fails. It's easy for this to happen almost invisibly. For example, if you create a temporary Message object in a function which is called many times in a program, you might assume that the objects are being destroyed at the return from the function. You'd be right about that, of course, but the free store memory will not be released.

The code for the destructor is as follows:

// Listing 02_02
// Destructor to free memory allocated by new
Message::~Message()
{
   cout << "Destructor called." // Just to track what happens
        << endl;
   delete[] pmessage;         // Free memory assigned to pointer
}

Because we're defining it outside of the class definition, we need to qualify the name of the destructor with the class name, Message, and the scope resolution operator. All the destructor does is display a message so that we can see what's going on and use the operator delete to free the memory pointed to by the member pmessage. Note that we must include the square brackets with delete because we're deleting an array (of type char).

Try It Out - Using the Message Class

We can exercise this class with a little example:

// Ex7_02.cpp
// Using a destructor to free memory
#include <iostream>               // For stream I/O
#include <string>                 // For strlen() and strcpy()
using namespace std;

// Put the Message class definition here (Listing 02_01)

// Put the destructor definition here (Listing 02_02)

int main()
{
   // Declare object
   Message Motto("A miss is as good as a mile.");

   // Dynamic object
   Message* pM = new Message("A cat can look at a queen.");

   Motto.ShowIt();    // Display 1st message
   pM->ShowIt();      // Display 2nd message
   cout << endl;

   // delete pM;      // Manually delete object created with new

   return 0;
}

How It Works

At the beginning of main(), we declare and define an initialized Message object, Motto, in the usual manner. In the second declaration we define a pointer to a Message object, pM, and allocate memory for the Message object pointed to by using the operator new. The call to new invokes the Message class constructor, which has the effect of calling new again to allocate space for the message text pointed to by the data member pmessage. If you build and execute this example, it will produce this:

We have only one destructor call, even though we created two message objects. We said earlier that the compiler doesn't take responsibility for objects created in the free store. The compiler arranged to call our destructor for the object Motto because this is a normal automatic object, even though the memory for the data member was allocated in the free store by the constructor. The object pointed to by pM is different. We allocated memory for the object in the free store, so we have to use delete to remove it. You need to uncomment the statement,

// delete pM;     // Manually delete object created with new

which appears just before the return statement in main(). If you run the code now, it will produce this:

Now we get an extra call of our destructor. This is surprising in a way. Clearly, delete is only dealing with the memory allocated by the call to new in the function main(). It only freed the memory pointed to by pM. Since our pointer to pM is a pointer to a Message object (for which a destructor has been defined), delete also calls our destructor to allow us to clean up the details of the members of the object. So when you use delete for an object created dynamically with new, it will always call the destructor for the object allocated on the free store.

Implementing a Copy Constructor

When you allocate space for class members dynamically, there are demons lurking in the free store. For our class Message, the default copy constructor is woefully inadequate. If we have these statements,

Message Motto1("Radiation fades your genes.");
Message Motto2(Motto1);

the effect of the default copy constructor will be to copy the address in the pointer member from Motto1 to Motto2. Consequently, there will be only one text string shared between the two objects, as illustrated in the diagram below:

If the string is changed from either of the objects, it will be changed for the other as well. If Motto1 is destroyed, the pointer in Motto2 will be pointing at a memory area which may now be used for something else, and chaos will surely ensue. Of course, the same problem arises if Motto2 is deleted; Motto1 would then contain a member pointing to a nonexistent string.

The solution is to supply a class copy constructor to replace the default version. This could be implemented in the public section of the class as follows:

Message(const Message& initM)     // Copy Constructor definition
{
   // Allocate space for text
   pmessage = new char[ strlen(initM.pmessage) + 1 ];
   // Copy text to new memory
   strcpy(pmessage, initM.pmessage);
}

You will remember from the previous chapter that, to avoid an infinite spiral of calls to the copy constructor, the parameter must be specified as a const reference. This copy constructor first allocates enough memory to hold the string in the object initM, storing the address in the data member of the new object, and then copies the text string from the initializing object. Now, our new object will be identical to, but quite independent of, the old one.

Just because you don't initialize one Message class object with another, don't think that you're safe and need not bother with the copy constructor. Another monster lurks in the free store. Consider the following statements,

Message Thought("Eye awl weighs yews my spell checker.");
DisplayMessage(Thought); // Call a function to output a message

where the function DisplayMessage() is defined as:

void DisplayMessage(Message LocalMsg)
{
   cout << endl << "The message is: ";
   LocalMsg.ShowIt ();
   return;
}

Looks simple enough doesn't it? What could be wrong with that? A catastrophic error, that's what! What the function DisplayMessage() does is actually irrelevant. The problem lies with the argument. The argument is a Message object, which is passed by value. With the default copy constructor, the sequence of events is as follows:

  1. The object Thought is created with the space for the message "Eye awl weighs yews my spell checker" allocated in the free store.

  2. The function DisplayMessage() is called and, because the argument is passed by value, a copy, LocalMsg, is made using the default copy constructor. Now the pointer in the copy points to the same string in the free store as the original object.

  3. At the end of the function, the local object goes out of scope, so the destructor for the Message class is called. This deletes the local object (the copy) by deleting the memory pointed to by the pointer pmessage.

  4. On return from the function DisplayMessage(), the pointer in the original object, Thought, still points to the memory area that has just been deleted. Next time you try to use the original object (or even if you don't, since it will need to be deleted sooner or later) your program will behave in weird and mysterious ways.

Any call to a function that passes by value an object of a class that has a member defined dynamically will cause problems. So, out of this, we have an absolutely 100 percent, 24 carat golden rule:

If you allocate space for a class member dynamically, always implement a copy constructor.


(Sharing Memory Between Variables)

Sharing Memory Between Variables

As a relic of the days when 64K was quite a lot of memory, we have a facility in C++ which allows more than one variable to share the same memory (but obviously not at the same time). This is called a union, and there are three basic ways in which you can use one:

  • First, you can use it so that a variable A occupies a block of memory at one point in a program, which is later occupied by another variable B of a different type, because A is no longer required. I recommend that you don't do this. It's not worth the risk of error that is implicit in such an arrangement. You can achieve the same effect by allocating memory dynamically.

  • Alternatively, you could have a situation in a program where a large array of data is required, but you don't know in advance of execution what the data type will be - it will be determined by the input data. I also recommend that you don't use unions in this case, since you can achieve the same result using a couple of pointers of different types and again allocating the memory dynamically.

  • A third possible use for a union is the one that you may need now and again - when you want to interpret the same data in two or more different ways. This could happen when you have a variable that is of type long, and you want to treat it as two values of type short. Windows will sometimes package two short values in a single parameter of type long passed to a function. Another instance arises when you want to treat a block of memory containing numeric data as a string of bytes, just to move it around.

Defining Unions

A union is defined using the keyword union. It is best understood by taking an example of a definition:

union shareLD        // Sharing memory between long and double
{
   double dval;
   long lval;
};

This defines a union type shareLD which provides for the variables of type long and double to occupy the same memory. The union type name is usually referred to as a tag name. This statement is rather like a class definition, in that we haven't actually defined a union instance yet, so we don't have any variables at this point. Once it has been defined, we can declare instances of a union in a declaration. For example:

shareLD MyUnion;

This declares an instance of the union, shareLD, that we defined previously. We could also have declared MyUnion by including it in the union definition statement:

union shareLD          // Sharing memory between long and double
{
   double dval;
   long lval;
} MyUnion;

If we want to refer to a member of the union, we use the direct member selection operator (the period) with the union instance name, just as we have done when accessing members of a class. So, we could set the long variable lval to 100 in the union instance MyUnion with this statement:

MyUnion.lval = 100;       // Using a member of a union

The basic problem with using a union to store different types of values in the same memory is that, because of the way a union works, you also need some means of determining which of the member values is current. This is usually achieved by maintaining another variable which acts as an indicator of the type of value stored.

A union is not limited to sharing between two variables. If you wish, you can share the same memory between several variables. The memory occupied by the union will be that which is required by its largest member. For example, if we define this union,

union shareDLF
{
   double dval;
   long lval;
   float fval;
} uinst = {1.5};

it will occupy 8 bytes, as illustrated in the figure here:

In the example, we defined an instance of the union, uinst, as well as the tag name for the union. We also initialized it with the value 1.5.

You can only initialize the first member of the union when you declare an instance.

Anonymous Unions

You can define a union without a union type name, in which case an instance of the union is automatically declared. For example, if we define a union like this,

union
{
   char* pval;
   double dval;
   long lval;
};

it defines both a union with no name and an instance of the union with no name. Consequently, the variables that it contains may be referred to just by their names, as they appear in the union definition. This can be more convenient than a normal union with a type name, but you need to be careful that you don't confuse the members with ordinary variables. The members of the union will still share the same memory. As an illustration of how the anonymous union above works, to use the double member, you could write this statement:

dval = 99.5;     // Using a member of an anonymous union

As you can see, there's nothing to distinguish the variable dval as a union member. If you need to use anonymous unions, you could use a naming convention to make the members more obvious.

Unions in Classes

You can include an instance of a union in a class. If you intend storing different types of value at different times, this usually necessitates maintaining a class data member to indicate what kind of value is stored in the union. There isn't usually a great deal to be gained by using unions as class members.


(Structs)

Structs

A language feature in C++ that is very similar to a class is the struct. This is essentially a hangover from the C language. (Windows was written in C before C++ became widely used, hence the struct appears pervasively in Windows programming.) The struct in C++, however, extends greatly the capability of the C struct - in fact, in C++ the keywords struct and class are are almost identical, and the reason struct is maintained C++ is for reasons of backward compatibility with C. The only difference between these two keywords is that the default access attribute of a struct is public, whereas, as we have already seen, the default access attribute for a class is private. A common use of structs in C++ is for classes that contain only public data members.


(Operator Overloading)

Operator Overloading

Operator overloading enables you to make standard operators, such as +, -, * and so on, work with objects of your own data types. It allows you to write a function which redefines a particular operator so that it performs a particular action when it's used with objects of a class. For example, you could redefine the operator > so that, when it was used with objects of the class Box (which we saw earlier), it would return true if the first Box argument had a greater volume than the second.

Operator overloading doesn't allow you to invent new operators, nor can you change the precedence of an operator, so your overloaded version of an operator will have the same priority in the sequence of evaluating an expression as the original base operator.

Although you can't overload all the operators, the restrictions aren't particularly oppressive. These are the operators that you can't overload:

  • The scope resolution operator, ::

  • The conditional operator, ?:

  • The direct member selection operator,.

  • The size-of operator, sizeof

  • The de-reference pointer to class member operator, .*

Anything else is fair game, which gives you quite a bit of scope. Obviously, it's a good idea to ensure that your versions of the standard operators are reasonably consistent with their normal usage, or at least reasonably intuitive in their operation. It wouldn't be a very sensible approach to produce an overloaded + operator that performed the equivalent of a multiply on class objects. The best way to understand how operator overloading works is to work through an example, so let's implement what we just referred to, the greater than operator, >, for the Box class.

Implementing an Overloaded Operator

If we want to implement an overloaded operator for a class, we have to write a special function. Assuming that it is a member of the class Box, the prototype for the function to overload the > operator will be as follows:

int operator>(Box& aBox);    // Overloaded 'greater than'

The word operator here is a keyword. Combined with an operator, in this case, >, it defines an operator function. The function name in this case is operator>(). You can write an operator function with or without a space between the keyword operator and the operator itself, as long as there's no ambiguity. The ambiguity arises with operators using normal letters such as new or delete. If they are written without a space, operatornew() or operatordelete(), they are legal names for ordinary functions, so for operator functions with these operators, you must leave a space between the keyword operator and the operator itself.

With our operator function operator>(), the right operand of the operator is that which is defined between parentheses. The left operand will be defined implicitly by the pointer this. So, if we have the following if statement,

if(Box1 > Box2)
   cout << endl << "Box1 is greater than Box2";

then the expression between parentheses in the if will call our operator function. It is equivalent to this function call:

Box1.operator>(Box2);

The correspondence between the Box objects in the expression and the operator function parameters is illustrated here:

Let's look at how the code for the operator>() function works:

// Operator function for 'greater than' which
// compares volumes of Box objects.
bool Box::operator>(const Box& aBox) const
{
   return (this->Volume()) > (aBox.Volume())
}

We use a reference parameter to the function to avoid unnecessary copying when the function is called. Note that we use the keyword const with this parameter, because we only want to compare the objects, we have no intention of altering either one of them. For the same reason, we declare the member function as const - by doing this, we are saying that this particular member function will not change the object on which it acts (the object pointed to by the this pointer), and the compiler will therefore flag any attempt to do so as an error. The return expression uses the member function Volume()to calculate the volume of the Box object pointed to by this, and compares the result, using the basic operator > , with the volume of the object aBox. Thus, true is returned if the Box object pointed to by the pointer this has a larger volume than the object aBox passed as a reference argument, and false otherwise. The member function Volume() also does not change the object on which it acts - it merely uses the length, breadth and height members to calculate the volume of the Box object. So, we should also declare Volume() as a const member function.

In fact, we would get a compile error in the operator>() function if we failed to do this, since we would be passing a const parameter to a function that had not promised not to modify it.

Try It Out - Operator Overloading

We can exercise this function with an example:

//Ex7_03.cpp
// Exercising the overloaded 'greater than' operator
#include <iostream>
using namespace std;

class Box               // Class definition at global scope
{
   public:
      // Constructor definition
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         cout << endl << "Constructor called.";
         length = lv;                   // Set values of
         breadth = bv;                  // data members
         height = hv;
      }

      // Function to calculate the volume of a box
      double Volume() const
      {
         return length * breadth * height;
      }

      // Overloaded 'greater than'
      bool operator>(const Box& aBox) const;

      // Destructor definition
      ~Box()
      { cout << "Destructor called." << endl; }

   private:
      double length;     // Length of a box in inches
      double breadth;    // Breadth of a box in inches
      double height;     // Height of a box in inches
};

// Operator function for 'greater than' which
// compares volumes of Box objects.
bool Box::operator>(const Box& aBox) const
{
   return (this->Volume()) > (aBox.Volume());
}

int main()
{
   Box SmallBox(4.0, 2.0, 1.0);
   Box MediumBox(10.0, 4.0, 2.0);
   Box BigBox(30.0, 20.0, 40.0);
   if(MediumBox > SmallBox)
      cout << endl
           << "MediumBox is Bigger than SmallBox";
   if(MediumBox > BigBox)
      cout << endl
           << "MediumBox is Bigger than BigBox";
   else
      cout << endl
           << "MediumBox is not Bigger than BigBox";
   cout << endl;
   return 0;
}

How It Works

The prototype of the operator function operator>() appears in the public section of the class. As the function definition is outside the class definition, it won't default to inline. This is quite arbitrary. We could just as well have put the definition in place of the prototype in the class definition. In this case, we wouldn't need to qualify the function name with Box:: in front of it. As you'll remember, this is needed in order to tell the compiler that the function is a member of the class Box.

The function main() has two if statements using the operator > with class members. These automatically invoke our overloaded operator. If you wanted to get confirmation of this, you could add an output statement to the operator function. The output from this example is:

The output demonstrates that the if statements work fine with our operator function, so being able to express the solution to Box problems directly in terms of Box objects is beginning to be a realistic proposition.

Implementing Full Support for an Operator

With our operator function operator>(), there are still a lot of things that you can't do. Specifying a problem solution in terms of Box objects might well involve statements such as the following:

if(aBox > 20.0)

Our function won't deal with that. If you try to use an expression comparing a Box object with a numerical value, you'll get an error message. In order to support this, we would need to write another version of the function operator>(), as an overloaded function.

We can quite easily support the type of expression that we've just seen. The prototype of the function would be:

// Compare a Box object with a constant
bool operator>(const double& value) const;

This would appear in the definition of the class. The Box object will be passed as the implicit pointer this.

The implementation is also easy. It's just one statement in the body of the function:

// Function to compare a Box object with a constant
bool Box::operator>(const double& value) const
{
   return (this->Volume()) > value;
}

This couldn't be much simpler, could it? But we still have a problem with the operator > with Box objects. We may well want to write statements such as this:

if(20.0 > aBox)
      ...                   // do something

You might argue that this could be done by implementing the operator function operator<() and rewriting the statement above to use it, which is quite true. Indeed, the < operator is likely to be a requirement for comparing Box objects anyway, but an implementation of support for an object type shouldn't artificially restrict the ways in which you can use the objects in an expression.

The use of the objects should be as natural as possible. The problem is how to do it. A member operator function always provides the left argument as the pointer this. Since the left argument, in this case, is of type double, we can't implement it as a member function. That leaves us with two choices: an ordinary function or a friend function. Since we don't need to access the private members of the class, we can implement it as an ordinary function. The prototype, placed outside the class definition, would need to be:

bool operator>(const double& value, const Box& aBox) const;

The implementation would be this:

// Function comparing a constant with a Box object
int operator>(const double& value, Box& aBox)
{
   return value > aBox.Volume();
}

As we've seen already, an ordinary function (and a friend function for that matter) accesses the public members of an object by using the direct member selection operator and the object name. The member function Volume() is public, so there's no problem using it here.

If the class didn't have the public function Volume(), we could either use a friend function that could access the private data members directly, or we could provide a set of member functions to return the values of the private data members and use those in an ordinary function to implement the comparison.

Try It Out - Complete Overloading of the > Operator

We can put all this together in an example to show how it works:

//Ex7_04.cpp
// Implementing a complete overloaded 'greater than' operator
#include <iostream>
using namespace std;

class Box                // Class definition at global scope
{
   public:
      // Constructor definition
      Box(double lv=1.0, double bv=1.0, double hv=1.0):
                                              length(lv),
                                              breadth(bv),
                                              height(hv)
      {
         cout << endl << "Constructor called.";
      }

      // Function to calculate the volume of a box
      double Volume() const
      {
         return length * breadth * height;
      }

      // Operator function for 'greater than' which
      // compares volumes of Box objects.
      bool operator>(const Box& aBox) const
      {
         return (this->Volume()) > (aBox.Volume());
      }

      // Function to compare a Box object with a constant
      bool operator>(const double& value) const
      {
         return (this->Volume()) > value;
      }

      // Destructor definition
      ~Box()
      { cout << "Destructor called." << endl;}

   private:
      double length;     // Length of a box in inches
      double breadth;    // Breadth of a box in inches
      double height;     // Height of a box in inches
};

bool operator>(const double& value, const Box& aBox);

int main()
{
   Box SmallBox(4.0,2.0, 1.0);
   Box MediumBox(10.0, 4.0, 2.0);

   if(MediumBox > SmallBox)
      cout << endl
           << "MediumBox is Bigger than SmallBox";

   if(MediumBox > 50.0)
      cout << endl
           << "MediumBox capacity is more than 50";
   else
      cout << endl
           << "MediumBox capacity is not more than 50";

   if(10.0 > SmallBox)
      cout << endl
           << "SmallBox capacity is less than 10";
   else
      cout << endl
           << "SmallBox capacity is not less than 10";

   cout << endl;
   return 0;
}

// Function comparing a constant with a Box object
bool operator>(const double& value, const Box& aBox)
{
   return value > aBox.Volume();
}

How It Works

The constructor for the class Box has been updated to initialize the data members rather than use assignments, since it's a bit more efficient. Note the position of the prototype for the ordinary function version of operator>(). It needs to follow the class definition, because it refers to a Box object in the parameter list. If you place it before the class definition, the example will not compile.

There is a way to place it at the beginning of the program file following the #include statement: use an incomplete class declaration. This would precede the prototype and would look like this:

class Box;               // Incomplete class declaration
bool operator>(const double& value, const Box& aBox);

The incomplete class declaration identifies Box to the compiler as a class and is sufficient to allow the compiler to process the prototype for the function properly, since it now knows that aBox is a variable of a user-defined type to be specified later.

This mechanism is also essential in circumstances where you have two classes, each of which has a pointer to an object of the other class as a member. They will each require the other to be declared first. It's possible to resolve such an impasse through the use of an incomplete class declaration.

The output from the example is:

After the constructor messages due to the declarations of the objects SmallBox and MediumBox, we have the output lines from the three if statements, each of which is working as we expected. The first of these is calling the operator function that is a class member and works with two Box objects. The second is calling the member function that has a parameter of type double. The expression in the third if statement calls the operator function that we implemented as an ordinary function.

As it happens, we could have made both the operator functions which are class members ordinary functions, since they only need access to the member function Volume(), which is public.

Any comparison operator can be implemented in much the same way as we have implemented these. They would only differ in the minor details and the general approach to implementing them would be exactly the same.

Overloading the Assignment Operator

If you don't provide an overloaded assignment operator function for your class, the compiler will provide a default. The default version will simply provide a member-by-member copying process, similar to that of the default copy constructor. However, don't confuse the default copy constructor with the default assignment operator. The default copy constructor is called by a declaration of a class object that's initialized with an existing object of the same class, or by passing an object to a function by value. The default assignment operator, on the other hand, is called when the left hand side and the right hand side of an assignment statement are objects of the same class.

For our Box class, the default assignment operator works with no problem, but for any class which has dynamically allocated members, you need to look carefully at the requirements of the class in question. There may be considerable potential for chaos in your program if you leave the assignment operator out under such circumstances.

For a moment, let's return to our Message class that we used when talking about copy constructors. You'll remember it had a member, pmessage, which was a pointer to a string. Now consider the effect that the default assignment operator could have. Suppose we had two instances of the class, Motto1 and Motto2. We could try setting the members of Motto2 equal to the members of Motto1 using the default assignment operator, as follows:

Motto2 = Motto1;          // Use default assignment operator

The effect of using the default assignment operator for this class is essentially the same as using the default copy constructor: disaster will result! Since each object will have a pointer to the same string, if the string is changed for one object, it's changed for both. There's also the problem that when one of the instances of the class is destroyed, its destructor will de-allocate the memory used for the string and the other object will be left with a pointer to memory that may now be used for something else.

What we need the assignment operator to do is to copy the text to a memory area owned by the destination object.

Fixing the Problem

We can fix this with our own assignment operator, which we will assume is defined within the class definition:

// Overloaded assignment operator for Message objects
Message& operator=(const Message& aMess)
{
   // Release memory for 1st operand
   delete[] pmessage;

   pmessage = new char[ strlen(aMess.pmessage) + 1];

   // Copy 2nd operand string to 1st
   strcpy(this->pmessage, aMess.pmessage);

   // Return a reference to 1st operand
   return *this;
}

You need to take note of a couple of subtleties here. The first is that we return a reference from the operator function. This is vital for two reasons.

First, we might need to use the result of the assignment operator on the right hand side of an expression. Consider a statement such as,

Motto1 = Motto2 = Motto3;

which will translate into,

Motto1 = (Motto2.operator=(Motto3));

with the result of the second operator function call on the right of the first equals sign. This, in turn, will finally become this:

Motto1.operator=(Motto2.operator=(Motto3));

Thus operator=() must return a usable value of the correct type.

The second reason (and the reason that we must return a reference) is that we may wish to use the result of the assignment on the left of another assignment. We discussed this principle in the previous chapter. Consider the following example:

(Motto1 = Motto2) = Motto3;

This translates into,

(Motto1.operator=(Motto2)) = Motto3;

and ultimately becomes:

(Motto1.operator=(Motto2)).operator=(Motto3);

The only way to cope with both of these situations in a standard and meaningful way is to return a reference.

Note that the C++ language does not enforce any restrictions on the accepted parameter or return types for the assignment operator, but it makes sense to declare the operator in the way just described.

The second subtlety you need to remember is that each object already has memory for a string allocated, so the first thing that the operator function has to do is to delete the memory allocated to the first object and then re-allocate sufficient memory to accommodate the string belonging to the second object. Once this is done, the string from the second object can be copied to the new memory now owned by the first.

There's still a defect in this operator function. What if we were to write the following statement?

Motto1 = Motto1;

Obviously, you wouldn't do anything as stupid as this directly, but it could easily be hidden behind a pointer, for instance, as in the following statement,

Motto1 = *pMess;

where the pointer pMess points to Motto1. In this case, the operator function as it stands would delete the memory for Motto1, allocate some more memory based on the length of the string that has already been deleted and try to copy the old memory which, by then, could well have been corrupted. We can fix this with a check at the beginning of the function, so now it would become this:

// Overloaded assignment operator for Message objects
Message& operator=(const Message& aMess)
{
   if(this == &aMess)            // Check addresses, if equal
      return *this;              // return the 1st operand

   // Release memory for 1st operand
   delete[] pmessage;

   pmessage = new char[ strlen(aMess.pmessage) +1];

   // Copy 2nd operand string to 1st
   strcpy(this->pmessage, aMess.pmessage);

   // Return a reference to 1st operand
   return *this;
}

Try It Out - Overloading the Assignment Operator

Let's put this together in a working example. We'll add a function, called Reset(), to the class at the same time. This just resets the message to a string of asterisks.

// Ex7_05.cpp
// Overloaded copy operator perfection
#include <iostream>
#include <string>
using namespace std;

class Message
{
   private:
      char* pmessage;      // Pointer to object text string

   public:
      // Function to display a message
      void ShowIt(void)
      {
         cout << endl << pmessage;
      }

      //Function to reset a message to *
      void Reset(void)
      {
         char* temp=pmessage;
         while(*temp)
            *(temp++)='*';
      }

      // Overloaded assignment operator for Message objects
      Message& operator=(const Message& aMess)
      {
         if(this == &aMess)         // Check addresses, if equal
            return *this;           // return the 1st operand

         // Release memory for 1st operand
         delete[] pmessage;

         pmessage = new char[ strlen(aMess.pmessage) +1];

         // Copy 2nd operand string to 1st
         strcpy(this->pmessage, aMess.pmessage);

         // Return a reference to 1st operand
         return *this;
      }

      // Constructor definition
      Message(const char* text = "Default message")
      {
         // Allocate space for text
         pmessage = new char[ strlen(text)+1 ];
         strcpy(pmessage, text);   // Copy text to new memory
      }

      // Destructor to free memory allocated by new
      ~Message()
      {
         // Just to track what happens
         cout << "Destructor called."
              << endl;
         delete[] pmessage;  // Free memory assigned to pointer
      }
};

int main()
{
   Message Motto1("The devil takes care of his own");
   Message Motto2;

   cout << "Motto2 contains - ";
   Motto2.ShowIt();

   cout << endl;
   Motto2 = Motto1;           // Use new assignment operator
   cout << "Motto2 contains - ";
   Motto2.ShowIt();
   cout << endl;

   Motto1.Reset(); // Setting Motto1 to * doesn't affect Motto2
   cout << "Motto1 now contains - ";
   Motto1.ShowIt();
   cout << endl;
   cout << "Motto2 still contains - ";
   Motto2.ShowIt();

   cout << endl;
   return 0;
}

You can see from the output of this program that everything works exactly as required, with no linking between the messages of the two objects, except where we explicitly set them equal.

So let's have another golden rule out of all of this:

Always implement an assignment operator if you allocate space dynamically for a data member of a class.

Having implemented the assignment operator, what happens with operations such as +=? Well, they don't work unless you implement them. For each form of op= that you want to use with your class objects, you need to write another operator function.

Overloading the Addition Operator

Let's look at overloading the addition operator for our Box class. This is interesting because it involves creating and returning a new object. The new object will be the sum (whatever we define that to mean) of the two Box objects that are its operands.

So what do we want the sum to mean? Let's define the sum of two Box objects as a Box object which is large enough to contain the other two boxes stacked on top of each other. We can do this by making the new object have a length member, which is the larger of the length members of the objects being added, and a breadth member derived in a similar way. The height member will be the sum of the height members of the two operand objects, so that the resultant Box object can contain the other two Box objects. This isn't necessarily an optimal solution, but it will be sufficient for our purposes. By altering the constructor, we'll also arrange that the length member of a Box object is always greater than or equal to the breadth member.

The addition operation is easier to explain graphically, so it's illustrated below:

Since we need to get at the members of an object directly, we will make the operator+() a member function. The prototype of the function will be this:

// Function adding two Box objects
Box operator+(const Box& aBox) const;

We define the parameter as a const reference to avoid unnecessary copying of the right argument when the function is called. We also declare the function itself to be const, as we are not going to change in anyway the object that appears on the left hand side of the addition operator. The operation function definition would now be as follows:

// Function to add two Box objects
Box Box::operator+(const Box& aBox) const
{
   // New object has the larger length and breadth,
   // and sum of heights
   return Box(length>aBox.length? length:aBox.length,
               breadth>aBox.breadth? breadth:aBox.breadth,
               height+aBox.height);
}

Try It Out - Exercising Our Addition

We can see how this works in an example:

// Ex7_06.cpp
// Adding Box objects
#include <iostream>
using namespace std;

class Box              // Class definition at global scope
{
   public:
      // Constructor definition
      Box(double lv=1.0, double bv=1.0, double hv=1.0):
                                                  height(hv)
      {
         length = lv > bv? lv: bv;       // Ensure that
         breadth = bv < lv? bv: lv;      // length >= breadth
      }

      // Function to calculate the volume of a box
      double Volume() const
      {
         return length * breadth * height;
      }

      // Operator function for 'greater than' which
      // compares volumes of Box objects.
      bool operator>(const Box& aBox) const
      {
         return (this->Volume()) > (aBox.Volume());
      }

      // Function to compare a Box object with a constant
      bool operator>(const double& value) const
      {
         return Volume() > value;
      }

      // Function to add two Box objects
      Box operator+(const Box& aBox) const
      {
         // New object has the larger length & breadth,
         // and sum of the heights
         return Box(length>aBox.length? length:aBox.length,
                     breadth>aBox.breadth? breadth:aBox.breadth,
                     height + aBox.height);
      }

      // Function to show the dimensions of a box
      void ShowBox(void) const
      {
         cout << length << " " << breadth << " " << height
              << endl;
      }

   private:
      double length;     // Length of a box in inches
      double breadth;    // Breadth of a box in inches
      double height;     // Height of a box in inches
};

bool operator>(const double& value, const Box& aBox);

int main(void)
{
   Box SmallBox(4.0, 2.0, 1.0);
   Box MediumBox(10.0, 4.0, 2.0);
   Box aBox;
   Box bBox;
   aBox = SmallBox+MediumBox;

   cout << "aBox dimensions are ";
   aBox.ShowBox();

   bBox = aBox+SmallBox+MediumBox;
   cout << "bBox dimensions are ";
   bBox.ShowBox();

   return 0;
}

// Function comparing a constant with a Box object
bool operator>(const double& value, const Box& aBox)
{
   return value > aBox.Volume();
}

How It Works

In this example we've changed the Box class members a little. The destructor has been deleted as it isn't necessary for this class, and the constructor has been modified to ensure that the length member isn't less than the breadth member. Knowing that the length of a box is always at least as big as the breadth makes the add operation a bit easier. We've also added the function ShowBox() to output the dimensions of a Box object. This will enable us to verify that our overloaded add operation is working as we expect.

The output from this program is:

This seems to be consistent with the notion of adding Box objects that we have defined and, as you can see, the function also works with multiple add operations in an expression. For the computation of bBox, the overloaded addition operator will be called twice.

We could equally well have implemented the add operation for the class as a friend function. Its prototype would then be this:

friend Box operator+(const Box& aBox, const Box& bBox);

The method to produce the result would be much the same, except that you'd need to use the direct member selection operator to obtain the members of the arguments to the function. It would work just as well as the first version of the operator function.


(Class Templates)

Class Templates

We saw back in Chapter 5 that we could define a function template which would automatically generate functions, varying only in the type of arguments accepted or in the type of value returned. C++ has a similar mechanism for classes. A class template is a sort of 'recipe' for a class. As you can see from the diagram, it's like the function template - it's a definition of a class that you use by specifying your choice of type for the variable parameter (T in this case). Doing this generates a particular instance of a class.

An appropriate class definition is generated when you instantiate an object of a template class for a particular type, so you can generate any number of different classes from one class template. We can see how this works in practice by looking at an example.

Defining a Class Template

We'll choose a simple example to illustrate how you define and use a class template, and we won't complicate things by worrying too much about possible errors that can arise if it's misused. Let's suppose we want to define classes which can store a number of data samples of some kind, and each class is to provide a max() function to calculate the maximum sample of those stored. This function will be similar to the one we saw in the function template discussion in Chapter 5. We can define a class template that will generate a class Samples for whatever type we want:

template <class T>
class Samples
{
   private:
      T values[100];       // Array to store samples
      int free;            // Index of free location in values

   public:
      // Constructor definition to accept an array of samples
      Samples(T vals[], int count)
      {
         free = count<100? count:100; // Don't exceed the array
         for(int i=0; i<free; i++)
            values[i] = vals[i]; // Store count number of samples
      }

      // Constructor to accept a single sample
      Samples(T val)
      {
         values[0] = val;     // Store the sample
         free = 1;            // Next is free
      }

      // Default constructor
      Samples()
      {
         free = 0;            // Nothing stored, so first is free
      }

      // Function to add a sample
      bool Add(T& val)
      {
         bool OK = free<100; // Indicates there is a free place
         if(OK)
            values[free++] = val; // OK true, so store the value
         return OK;
      }

      // Function to obtain maximum sample
      T Max()
      {
         T theMax = values[0];     // Set first sample as maximum
         for(int i=1; i<free; i++)  // Check all the samples
         if(values[i]>theMax)
            theMax = values[i];       // Store any larger sample
         return theMax;
      }
};

To indicate we're defining a template rather than a straightforward class definition, we insert the keyword template and the type parameter, T, between angled brackets, just before the keyword class and the class name, Samples. This is the same syntax that we used to define a function template back in Chapter 5. The parameter T is the type variable that you'll replace by a specific type when you declare a class object. Wherever the parameter T appears in the class definition, it will be replaced by the type that you specify in your object declaration; this creates a class definition corresponding to this type. You can specify any type (a basic data type or a class type), but it has to make sense in the context of the class template, of course. Any class type that you use to instantiate a class from a template must have all the operators defined that the member functions of the template will use with such objects. If your class hasn't implemented operator>(), for example, it will not work with our class template above. In general, you can specify multiple parameters in a class template if you need them. We'll come back to this possibility a little later in the chapter.

Getting back to our example, the type of the array in which to store the samples is specified as T. The array will therefore be an array of whatever type you specify for T when you declare a Samples object. As you can see, we also use the type T in two of the constructors for the class, as well as in the Add() and Max() functions. Each of these occurrences will also be replaced when you instantiate a class object. The constructors we've defined support the creation of an empty object, an object with a single sample and an object initialized with an array of samples. The Add() function allows samples to be added to an object, one at a time. You could also overload this function to add an array of samples. The class template includes some elementary provision to prevent the capacity of the values array being exceeded in the Add() function, and in the constructor that accepts an array of samples.

As we said earlier, in theory you can create objects of Samples classes that will handle any data type: int, double, or any class type that you've defined. In practice, this doesn't mean it will necessarily compile and work as you expect. It all depends on what the template definition does, and usually a template will only work for a particular range of types. For example, the Max() function implicitly assumes that the > operator is available for whatever type is being processed. If it isn't, your program will not compile. Clearly, you'll usually be in the position of defining a template that works for some types but not others, but there's no way you can restrict what type is applied to a template.

Template Member Functions

You may want to place the definition of a class template member function outside of the template definition. The syntax for this isn't particularly obvious, so let's look at how you do it. The function declaration appears in the class template definition in the normal way. For instance:

template <class T>
class Samples
{
   // Rest of the template definition...
   T Max();// Function to obtain maximum sample
   // Rest of the template definition...
}

This declares the Max() function within the class. You now need to create a separate function template for the definition of the member function. You must use the template class name plus the parameters in angled brackets to identify the class to which the function belongs:

template<class T>
T Samples<T>::Max()
{
   T theMax = values[0];        // Set first sample as maximum
   for(int i=1; i<free; i++)    // Check all the samples
      if(values[i]>theMax)
         theMax = values[i];    // Store any larger sample
   return theMax;
}

You saw the syntax for a function template back in Chapter 5. The function template definition here should have the same parameters as the class template. There's just one in this case, but in general there can be several. Note how you only put the parameter name, T, along with the class name before the scope resolution operator. This is necessary - the parameters are fundamental to the identification of the class to which a function, produced from the template, belongs. Your type is plugged into the class template to generate the class definition, and into the function template to generate the definition for the Max() function for the class. Each class that's produced from the class template needs to have its own definition for the function Max().

Defining a constructor or a destructor outside of the class template definition is very similar. We could write the definition of the constructor accepting an array as:

template<class T>
Samples<T>::Samples(T vals[], int count)
{
   free = count<100? count:100;   // Don't exceed the array
   for(int i=0; i<m_Free; i++)
      values[i] = vals[i];       // Store count number of samples
}

The class to which the constructor belongs is specified in the template in the same way as for an ordinary member function. Note that the constructor name doesn't require the parameter specification. You only use the parameter with the class name preceding the scope resolution operator.

Creating Objects from a Class Template

When we used a function defined by a function template, the compiler was able to generate the function from the types of the arguments used. The type parameter for the function template was implicitly defined by the specific use of a particular function. Class templates are a little different. To create an object based on a class template, you must always specify the type parameter following the class name in the declaration.

For example, to declare a Samples object to handle samples of type double, you could write the declaration as:

Samples<double> MyData(10.0);

This defines a Samples object that can store samples of type double, and the object is created with one sample stored with the value 10.0.

Try It Out - Class Templating

You could create a Samples object that stores our Box objects. This will work because the Box class implements the operator>() function to overload the greater-than operator. We could exercise the class template with the main() function in the following listing:

// Ex7_07.cpp
// Trying out a class template
#include <iostream>
using namespace std;

// Put the Box class definition here

// Put the Samples class template definition here

int main()
{
   Box Boxes[] = {                    // Create an array of boxes
                   Box(8.0, 5.0, 2.0), // Initialize the boxes...
                   Box(5.0, 4.0, 6.0),
                   Box(4.0, 3.0, 3.0)
                  };

   // Create the Samples object to hold Box objects
   Samples<Box> MyBoxes(Boxes, sizeof Boxes/sizeof Box);
   Box MaxBox = MyBoxes.Max();           // Get the biggest box
   cout << endl                          // and output its volume
        << "The biggest box has a volume of "
        << MaxBox.Volume()

        << endl;
   return 0;
}

Here we create an array of three Box objects and then use this array to initialize a Samples object that can store Box objects. The declaration of the Samples object is basically the same as it would be for an ordinary class, but with the addition of the type parameter in angled brackets following the template class name.

The two pieces of code you should replace the comments with are:

  • The Box class definition from Ex7_06.cpp

  • The class template we wrote for Samples before we started talking about template member functions being defined outside the template definition

The program will generate the following output:

You could try modifying the example by implementing function member definitions outside the template, and perhaps seeing what happens when you instantiate classes by using the template with various other types.

You might be surprised at what happens if you add some output statements to the class constructors. The constructor for the Box is being called 103 times! Look at what we are doing in main(). First we create an array of 3 Box objects, so that's 3 calls. We then create a Samples object to hold them, but a Samples object contains an array of 100 variables of type Box, so we call the constructor another 100 times.

Class Templates with Multiple Parameters

Using multiple type parameters in a class template is a straightforward extension of the example using a single parameter, which we have just seen. You can use each of the type parameters wherever you want in the template definition. For example, you could define a class template with two type parameters:

template<class T1, class T2>
class ExampleClass
{
   // Class data members
   private:
      T1 value1;
      T2 value2;
      // Rest of the template definition...
};

The types of the two class data members shown will be determined by the types you supply for the parameters when you instantiate an object.

The parameters in a class template aren't limited to types. You can also use parameters that require constants or constant expressions to be substituted in the class definition. In our Samples template, we arbitrarily defined the values array with 100 elements. We could, however, let the user of the template choose the size of the array when the object is instantiated, by defining the template as:

template <class T, int Size> class Samples
{
   private:
      T values[Size];      // Array to store samples
      int free;            // Index of free location in values

   public:
      // Constructor definition to accept an array of samples
      Samples(T vals[], int count)
      {
         free = count<Size? count:Size; // Don't exceed the array
         for(int i=0; i<m_Free; i++)
            values[i] = vals[i]; // Store count number of samples
      }

      // Constructor to accept a single sample
      Samples(T val)
      {
         values[0] = val;   // Store the sample
         free = 1;          // Next is free
      }

      // Default constructor
      Samples()
      {
         free = 0;          // Nothing stored, so first is free
      }

      // Function to add a sample
      bool Add(T& val)
      {
         bool OK = free<Size; // Indicates there is a free place
         if(OK)
            values[free++] = val; // OK true, so store the value
         return OK;
      }

      // Function to obtain maximum sample
      T Max()
      {
         T theMax = values[0];     // Set first sample as maximum
         for(int i=1; i<free; i++) // Check all the samples
            if(values[i]>theMax)
               theMax = values[i]; // Store any larger sample
         return theMax;
      }
};

The value supplied for Size when you create an object will replace the appearance of the parameter throughout the template definition. Now we can declare the Samples object from the previous example as:

Samples<Box, 3> MyBoxes(Boxes, sizeof Boxes/sizeof Box);

Since we can supply any constant expression for the Size parameter, we could also have written this as:

Samples<Box, sizeof Boxes/sizeof Box>
MyBoxes(Boxes, sizeof Boxes/sizeof Box);

You need to be careful with expressions that involve comparison operators. A statement such as:

Samples<aType, x>y? 10:20> MyType();      // Wrong!

will not compile correctly because the > in the expression will be interpreted as a right angled bracket. Instead, you should write this as:

Samples<aType, (x>y? 10:20)> MyType();    // OK

The parentheses make sure that the expression for the second template argument doesn't get mixed up with the angled brackets.


(Using Classes)

Using Classes

We've touched on most of the basic aspects of defining a class, so maybe we should look at how a class might be used to solve a problem. We'll need to keep the problem simple in order to keep this book down to a reasonable number of pages, so we'll consider problems in which we can use an extended version of the Box class.

The Idea of a Class Interface

The implementation of an extended Box class should incorporate the notion of a class interface. What we're going to provide is a tool kit for anyone wanting to work with Box objects, so we need to assemble a set of functions that represents the interface to the world of boxes. Since the interface will represent the only way to deal with Box objects, it needs to be defined to adequately cover the likely things one would want to do with a Box object, and be implemented, as far as possible, in a manner that protects against misuse or accidental errors.

The first question that we need to consider is the nature of the problem we intend to solve and, from that, derive the kind of functionality we need to provide in the class interface.

Defining the Problem

The principal function of a box is to contain objects of one kind or another so, in a word, our problem is packaging. We'll attempt to provide a class that eases packaging problems in general and then see how it might be used. We'll assume that we'll always be working on packing Box objects into other Box objects since, if you want to pack candy in a box, you could always represent each of the pieces of candy as an idealized Box object. The basic operations that we might want to provide for our Box class include:

  • Calculate the volume of a Box. This is a fundamental characteristic of a Box object and we have an implementation of this already.

  • Compare the volumes of two Box objects to determine which is the larger. We probably should support a complete set of comparison operators for Box objects. We already have a version of the > operator.

  • Compare the volume of a Box object with a specified value and vice versa. We also have an implementation of this for the > operator, but we will also need the other comparison operators.

  • Add two Box objects to produce a Box object which will contain both the original objects. Thus, the result will be at least the sum of the volumes, but may be larger. We have a version of this already by overloading the + operator.

  • Multiply a Box object by an integer (and vice versa) to provide a Box object which will contain a specified number of the original objects. This is effectively designing a carton.

  • Determine how many Box objects of a given size can be packed in another Box object of a given size. This is effectively division, so we could overload the / operator.

  • Determine the volume of space remaining in a Box object after packing it with the maximum number of Box objects of a given size.

We had better stop right there! There are undoubtedly other functions that would be very useful but, in the interest of saving trees, we'll consider the set complete, apart from ancillaries such as accessing dimensions, for example.

Implementing the Box Class

We really need to consider the degree of error protection that we want to build into the Box class. The basic class that we defined to illustrate various aspects of classes is a starting point, but we should also consider some points a little more deeply. The constructor is a little weak in that it doesn't ensure that we have valid dimensions for a Box, so perhaps the first thing we should do is to ensure we always have valid objects. We could redefine the basic class as follows:

class Box             // Class definition at global scope
{
   public:
      // Constructor definition
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         lv = lv<=0? 1.0: lv;              // Ensure positive
         bv = bv<=0? 1.0: bv;              // dimensions for
         hv = hv<=0? 1.0: hv;              // the object
         length = lv>bv? lv: bv;         // Ensure that
         breadth = bv < lv? bv: lv;      // length >= breadth
         height = hv;
      }

      // Function to calculate the volume of a box
      double Volume() const
      {
         return length * breadth * height;
      }

      // Function providing the length of a box
      double GetLength() const { return length; }
      // Function providing the breadth of a box
      double GetBreadth() const { return breadth; }
      // Function providing the height of a box
      double GetHeight() const { return height; }

   private:
      double length;            // Length of a box in inches
      double breadth;           // Breadth of a box in inches
      double height;            // Height of a box in inches
};

Our constructor is now secure, since any dimension that the user of the class tries to set to a negative number or zero will be set to 1 in the constructor. You might also consider displaying a message for a negative or zero dimension, since there's obviously an error when this occurs, and arbitrarily and silently setting a dimension to 1 might not be the best solution.

The default copy constructor is satisfactory for our class, since we have no dynamic memory allocation for data members, and the default assignment operator will also work as we would like. The default destructor also works perfectly well in this case, so we don't need to define it. Perhaps now we should consider comparisons of objects of our class.

Comparing Box Objects

We should include support for >, >=, ==, < and <= to operate between two Box objects, as well between a Box object and a value of type double. We should implement these as ordinary global functions, since they don't need to be member functions. We can also write the functions to compare two Box objects in terms of the functions to compare a Box object with a double value, so let's start with the latter. We can repeat the operator>() function that we had before:

// Function for testing if a constant is > a Box object
bool operator>(const double& value, const Box& aBox)
{
   return value > aBox.Volume();
}

We can now write the operator<() function in a similar way:

// Function for testing if a constant is < Box object
bool operator<(const double& value, const Box& aBox)
{
   return value < aBox.Volume();
}

The implementation of the same operators, but with the arguments reversed, can now be specified using these two functions:

// Function for testing if Box object is > a constant
bool operator>(const Box& aBox, const double& value)
{ return value<aBox; }

// Function for testing if Box object is < a constant
bool operator<(const Box& aBox, const double& value)
{ return value>aBox; }

We just use the appropriate overloaded operator function that we wrote before, with the arguments from the call to the new function switched.

>= and <= will be the same as the first two functions but with the <= operator replacing each use of <, and >= instead of >; there's little point in reproducing them at this stage. The operator==() functions are also very similar:

// Function for testing if constant is == a Box object
bool operator==(const double& value, const Box& aBox)
{
   return value == aBox.Volume();
}

// Function for testing if Box object is == a constant
bool operator==(const Box& aBox, const double& value)
{
   return value == aBox;
}

We now have a complete set of comparison operators for Box objects. Keep in mind that these will also work with expressions as long as the expressions result in objects of the required type, so we will be able to combine them with the use of other overloaded operators.

Combining Box Objects

Now we come to the question of overloading the operators +, *, /, and %. We will take them in order. The add operation that we already have from Ex7_06.cpp has this prototype:

// Function adding two Box objects
Box operator+(const Box& aBox) const;

Although our original implementation of this isn't an ideal solution, we'll use it to avoid overcomplicating our class. A better version would need to see if the operands had any faces with the same dimensions and join along those faces, but coding that could get a bit messy. Of course, if this were a practical application, a better add operation could be developed later and substituted for the existing version, and any programs written using the original would still run without change. The separation of the interface to a class from its implementation is crucial to good C++ programming.

You'll have noticed that we conveniently forgot the subtraction operator. This is a judicious oversight to avoid the complications inherent in implementing this. If you're really enthusiastic about it, and you think it's a sensible idea, you can give it a try - but you need to decide what to do when the result has a negative volume. If you allow the concept, you need to resolve which box dimension, or dimensions, are to be negative, and how such a box is to be handled in subsequent operations.

The multiply operation is very easy. It represents the process of creating a box to contain n boxes, where n is the multiplier. The simplest solution would be to take the length and breadth of the object to be packed and multiply the height by n to get the new Box object. We'll make it a little cleverer by checking whether or not the multiplier is even and, if it is, stacking the boxes side by side by doubling the breadth value and only multiplying the height value by half of n. This is illustrated here:

Of course, we don't need to check which is the larger of the length and breadth for the new object, since the constructor will sort it out automatically for us. We'll write the version of the operator function, operator*(), as a member function, with the left operand as a Box object:

// Box multiply operator this*n
Box operator*(int n) const
{
   if(n%2)
      return Box(length, breadth, n*height);       // n odd
   else
      return Box(length,2.0*breadth,(n/2)*height); // n even
}

Here, we use the % operator to determine whether n is even or odd. If n is odd, the value of n%2 is 1 and the if statement is true. If it's even, n%2 is 0 and the statement is false.

We can now use the function we've just written in the implementation of the version with the left operand as an integer. We can write this as a non-member function:

// Box multiply operator n*aBox
Box operator*(int n, const Box aBox)
{
   return aBox*n;
}

This version of the multiply operation simply reverses the order of the operands so as to use the previous version of the function directly. That completes the set of combinatorial operators for Box objects that we defined. We can finally look at the two analytical operator functions, operator/() and operator%().

Analyzing Box Objects

As we've said, the division operation will determine how many Box objects, given by the right operand can be contained in the Box object specified by the left operand. To keep it relatively simple, we'll assume that all the Box objects are packed the right way up, that is, with the height dimensions vertical. We'll also assume that they are all packed the same way round, so that their length dimensions are aligned. Without these assumptions, it can get rather complicated.

The problem will then amount to determining how many of the right-operand objects can be placed in a single layer, and then deciding how many layers we can get inside the left-operand Box.

We'll code this as a member function as follows:

int operator/(const Box& aBox) const
{
   // Temporary for number in horizontal plane this way
   int tc1 = 0;
   // Temporary for number in a plane that way
   int tc2 = 0;

   tc1 = static_cast<int>(length/aBox.length)*     // to fit this
           static_cast<int>(breadth/aBox.breadth); // way...
   tc2 = static_cast<int>(length/aBox.breadth)*    // ... and
           static_cast<int>(breadth/aBox.length);  // that way

   //Return best fit
   return static_cast_<int>((height/aBox.height)
                                           *(tc1>tc2? tc1:tc2));
}

This function first determines how many of the right-operand Box objects can fit in a layer with their lengths aligned with the length dimension of the left-operand Box. This is stored in tc1. We then calculate how many can fit in a layer with the lengths of the right-operand Boxes lying in the breadth direction of the left-operand Box. We then multiply the larger of tc1 and tc2 by the number of layers we can pack in, and return that value. This process is illustrated here:

We look at two possibilities: fitting bBox into aBox with the length aligned with that of aBox, and then with the length of bBox aligned with the breadth of aBox. You can see from the illustration that the best packing results from rotating bBox, so that the breadth divides into the length of aBox.

The other analytical operator function, operator%(), for obtaining the free volume in a packed aBox is easier, since we can use the operator we've just written. We can write it as an ordinary global function, since we don't need access to the private members of the class.

// Operator to return the free volume in a packed box
double operator%(const Box& aBox, const Box& bBox)
{
   return aBox.Volume() - (aBox/bBox) * bBox.Volume();
}

This computation falls out very easily using existing class functions. The result is the volume of the big box, aBox, minus the volume of the bBox boxes in it. The number of bBox objects packed is given by the expression aBox/bBox, which uses the previous overloaded operator. We multiply this by the volume of bBox objects to get the volume to be subtracted from the volume of the large box, aBox.

That completes our class interface. Clearly, there are many more functions that might be required for a production problem solver but, as an interesting working model demonstrating how we can produce a class for solving a particular kind of problem, it will suffice. Now we should go ahead and try it out on a real problem.

Try It Out - A Multifile Project Using the Box Class

Before we can actually start writing the code to use our Box class and its overloaded operators, the first thing we need to do is to assemble the definition for the class into a coherent whole. We're going to take a rather different approach from what you've seen previously, in that we're going to write multiple files for our project. We're also going to start using the facilities that Visual C++ provides for creating and maintaining code for our classes. This will mean that you do rather less of the work, but it will also mean that the code will be slightly different in places.

Start by creating a new project for a console application called Ex7_08. You'll see the left hand window shown here:

This appears on the ClassView tab, which shows a view of all the classes in a project. The next tab to the right is the FileView tab, which presents the files that go to make up the project. The rightmost tab is the InfoView tab, through which you can access the online documentation. Although there are no classes defined - or anything else for that matter - Visual C++ has already made provision for including some. We can use Visual C++ to create a skeleton for our Box class, and the files that relate to it too. Right click anywhere on [image] in ClassView, and select New Class... from the pop-up menu that appears. You will then be able to enter the name of the class that we want to create, Box, in the New Class dialog as shown here.

The name of the file that's indicated on the dialog, Box.cpp, will be used to contain the class implementation, which consists of the definitions for the function members of the class. This is the executable code for the class. You can change the name of this file by selecting the Change... button if you want, but Box.cpp looks like a good name for the file in this case. The class definition will be stored in a file called Box.h. This is the standard way of structuring a program. Code which consists of class definitions is stored in files with the extension .h, and code which defines functions is stored in files with the extension .cpp. Usually, each class definition goes in its own .h file, and each class implementation goes in its own .cpp file.

When you click on the OK button in the dialog several things will happen:

  1. A file Box.h will be created containing a skeleton definition for the class Box

  2. A file Box.cpp will be created containing a skeleton implementation for the class

  3. The Wizard bar will be activated

The Wizard bar should already be displayed, but if it isn't, right click in the menu area of the window and click on Wizard Bar in the pop-up. The Wizard Bar is used like this:

As you can see, the Wizard Bar provides a range of tools for accessing and modifying your code. You can add classes, you can add members to classes, and you can switch between the definition and the implementation of member functions. You can also get any global entity in your program displayed in the editor window. Of course, you should only use the Wizard Bar when it's convenient to do so - you always have the option of displaying a file and modifying your source code directly in the editor window.

Let's start developing our Box class based on what Visual C++ has provided automatically for us.

Defining the Box Class

If you click on the + to the left of [image] in the ClassView, the tree will be expanded and you will see that Box is now defined for the project. All the classes in a project are displayed in this tree. We can view the source code supplied for the definition of the class by double clicking the class name in the tree, or by clicking on the button in the Wizard Bar. The code is as follows:

// Box.h: interface for the Box class.
//
//////////////////////////////////////////////////////////////////////

#if !defined(AFX_BOX_H__A1FA3EA0_CEFB_11D1_915E_00A0C94515AD__INCLUDED_)
#define AFX_BOX_H__A1FA3EA0_CEFB_11D1_915E_00A0C94515AD__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

class Box
{
public:
   Box();
   virtual ~Box();
};

#endif // !defined(AFX_BOX_H__A1FA3EA0_CEFB_11D1_915E_00A0C94515AD__INCLUDED_)

After the comment lines, there are some preprocessor directives. The first is a test whether the symbol beginning AFX_BOX_H__... has not been defined - #if !defined is a directive that implements an 'if not defined' test, and works rather like an ordinary if statement. If the symbol in the directive doesn't already exist, all the text lines down to the #endif directive will be included, including the #define directive. If the symbol has been defined, the lines down to the #endif directive will be skipped.

The effect of this arrangement is to prevent the code in the file from being included into a file more than once. The first time the code is included, the symbol beginning AFX_BOX_H__... gets defined, thus preventing any further inclusions of the same code. This is pretty much standard procedure in .h files, and you should write all your .h files with this sort of protection built in, so that you avoid the problems that duplicate definitions cause. Visual C++ will always include it automatically in the files for class definitions that it creates. Don't worry that numbers and letters in the symbol are different on your machine; this is done to make absolutely sure the symbol is unique.

The second pair of #if-#endif directives performs a similar function, but one which is only available on more recent compilers - that's what _MSC_VER denotes. If the compiler is sufficiently recent (and Visual C++ 5 most certainly is!), the #pragma once directive is executed, which means that this file doesn't even have to be opened again. You can see that this is faster than opening the file and testing the symbol every time the compiler comes across a #include for it.

Within the code, we have an outline class definition, including the default class constructor and the destructor. The destructor has been declared as virtual, but we'll defer discussion of what that means until the next chapter. You could delete the keyword if you want, as it isn't needed here, but it will do no harm if you leave it in.

Note that the button on the Wizard Bar has changed. Clicking on the button while the class definition is displayed will switch to the .cpp file. In fact, it does better than that, although the full benefit isn't clear while your files are as short as they are at this stage. The button switches you to the position in the .cpp file which contains the definition of the function at the current cursor position, with the function header highlighted. Clicking on it again will toggle back to the class definition.

Adding Data Members

First, we'll add the private data members length, breadth, and height. Right click on [image] (in class view) and select Add Member Variable... from the pop-up menu. You can then specify the first data member that we want to add to the class in the dialog.

The variable type appears in the upper edit box, and the access specifier is selected from the three radio buttons at the bottom of the dialog. When you click on the OK button, the variable will be added to the class definition. Repeat the process for the other two class data members. You can then add comments to these directly in the editor window if you wish:

// Box.h: interface for the Box class.
//
//////////////////////////////////////////////////////////////////////

#if !defined(AFX_BOX_H__A1FA3EA0_CEFB_11D1_915E_00A0C94515AD__INCLUDED_)
#define AFX_BOX_H__A1FA3EA0_CEFB_11D1_915E_00A0C94515AD__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

class Box
{
public:
   Box();
   virtual ~Box();

private:
   double length;
   double breadth;
   double height;
};

#endif // !defined(AFX_BOX_H__A1FA3EA0_CEFB_11D1_915E_00A0C94515AD__INCLUDED_)

Of course, you're quite free to enter the declarations for these members manually, directly into the code, if you want.

Defining the Constructor

We want to change the declaration of the constructor in the class definition, so modify it to:

Box(double lv=1.0, double bv=1.0, double hv=1.0);

Now we're ready to implement it - modify the constructor definition to:

Box::Box(double lv, double bv, double hv)
{
   lv = lv<=0.0? 1.0:lv;            // Ensure positive
   bv = bv<=0.0? 1.0:bv;            // dimensions for
   hv = hv<=0.0? 1.0:hv;            // the object
   length = lv>bv? lv:bv;           // Ensure that
   breadth = bv<lv? bv:lv;          // length >= breadth
   height = hv;
}

Remember that the initializers for the parameters to a member function should only appear in the member declaration in the class definition, not in the definition of the function. If you put them in the function definition, your code will not compile. We've seen this code already, so we won't discuss it again. It would be a good idea to save the file at this point by clicking on the Save toolbar button. Get into the habit of saving the file you're editing before you switch to something else.

Adding Function Members

We need to add all the functions you saw earlier to the Box class. Previously, we defined several function members within the class definition, so that these functions were automatically inline. However, if we just add code to the body of each function definition in the .cpp file, the functions won't be inline, and for those that are very short, such as Volume(), GetLength(), GetBreadth(), and GetHeight(), it really would be much better if they were.

You might think it was simply a question of adding the keyword inline to the function definitions, but the problem here is that inline functions end up not being 'real' functions. Because the code from the body of each function has to be inserted directly at the position it is called, the definitions of the functions need to be available when the file containing calls to the functions is compiled. If they're not, you'll get linker errors and your program will not run. If you want member functions to be inline, you must include the function definitions in the .h file for the class. They can be defined either within the class definition, or immediately following it in the .h file. You should put any global inline functions you need into a .h file, and #include that file into any .cpp file that uses them. We'll put the definitions for the functions we want to make inline in the Box class definition, so modify it to:

class Box
{
public:
   // Get dimensions of Box
   double GetHeight(void) const { return height; }
   double GetBreadth(void) const { return breadth; }
   double GetLength(void) const { return length; }

   // Calculate volume
   double Volume(void) const
   { return length * breadth * height; }

   Box(double lv=1.0, double bv=1.0, double hv=1.0);
   virtual ~Box();

private:
   double height;      // Height of a box in inches
   double breadth;     // Breadth of a box in inches
   double length;      // Length of a box in inches
};

You could enter the other member functions we need directly in the editor window, but you can also use the Wizard Bar to do it, and the practice will be useful. Click on the down arrow in the Wizard Bar and select Add Member Function... from the pop-up. You can then enter the details of the first function we want to add in the dialog that appears:

Here we've defined the operator+() function as public with a return type of Box. Note the options to declare a function as static or virtual. As you know, a static member function exists independently of any objects of a class. We'll get to virtual functions in Chapter 10. When you click on OK, the declaration for the function will be added to the class definition in the Box.h file, and a skeleton definition for the function will be added to the Box.cpp file.

You need to repeat this process for each of the other member functions of Box, so the class definition will look like this:

class Box
{
public:
   // Divide one box into another
   int operator/(const Box& aBox) const;
   // Multiply a box by an integer
   Box operator*(int n) const ;
   // Add two boxes
   Box operator+(const Box& aBox) const ;

   // Get dimensions of Box
   double GetHeight(void) const { return height; }
   double GetBreadth(void) const { return breadth; }
   double GetLength(void) const { return length; }

   // Calculate volume
   double Volume(void) const
   { return length * breadth * height; }

   Box(double lv=1.0, double bv=1.0, double hv=1.0);
   virtual ~Box();

private:
   double height;
   double breadth;
   double length;
};

The comments were added manually here, as were the consts that appear after the member functions that do not alter the object in any way. The skeleton implementations of these three functions have been placed in Box.cpp. Switch to the .cpp file and define the functions with the implementations we've already discussed. The .cpp file should then contain the following code (where the highlighted areas indicate the code that you need to add yourself):

// Box.cpp: implementation of the Box class.
//
//////////////////////////////////////////////////////////////////////

#include "Box.h"

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

Box::Box(double lv, double bv, double hv)
{
   lv = lv<=0.0? 1.0:lv;            // Ensure positive
   bv = bv<=0.0? 1.0:bv;            // dimensions for
   hv = hv<=0.0? 1.0:hv;            // the object
   length = lv>bv? lv:bv;          // Ensure that
   breadth = bv<lv? bv:lv;         // length >= breadth
   height = hv;
}

Box::~Box()
{
}

Box Box::operator +(const Box& aBox) const
{
   // New object has larger length and breadth of the two,
   // and sum of the two heights
   return Box(length>aBox.length? length:aBox.length,
               breadth>aBox.breadth? breadth:aBox.breadth,
               height+aBox.height);
}

Box Box::operator *(int n) const
{
   if(n%2)
      return Box(length, breadth, n*height);        // n odd
   else
      return Box(length, 2.0*breadth,(n/2)*height); // n even
}

int Box::operator /(const Box& aBox) const
{
   // Temporary for number in horizontal plane this way
   int tc1 = 0;
   // Temporary for number in a plane that way
   int tc2 = 0;

   tc1 = static_cast<int>(length/aBox.length)*     // to fit this
           static_cast<int>(breadth/aBox.breadth); // way...
   tc2 = static_cast<int>(length/aBox.breadth)*    // ...and that
           static_cast<int>(breadth/aBox.length);  // way

   //Return best fit
   return static_cast<int>(height/aBox.height)
                                         *(tc1>tc2? tc1:tc2);
}

The very short functions, particularly those that just return the value of a data member, have their definitions within the class definition so that they are inline. If you take a look at ClassView by clicking on the tab, and then click on the + beside the Box class name, you'll see that all the members of the class are shown.

This completes the Box class, but we still need to define the global functions that implement operators to compare the volume of a Box object with a numerical value.

Adding Global Functions

We need to create a .cpp file containing the definitions for the global functions supporting operations on Box objects. Click on the new text file menu button to open a new text file, enter the following code in the editor window, and save the file as BoxOperators.cpp:

// BoxOperators.cpp
// Box object operations that don't rely on private members
#include "Box.h"

// Function for testing if a constant is > a Box object
int operator>(const double& value, const Box& aBox)
{ return value > aBox.Volume(); }

// Function for testing if a constant is < Box object
int operator<(const double& value, const Box& aBox)
{ return value < aBox.Volume(); }

// Function for testing if Box object is > a constant
int operator>(const Box& aBox, const double& value)
{ return value < aBox; }

// Function for testing if Box object is < a constant
int operator<(const Box& aBox, const double& value)
{ return value > aBox; }

// Function for testing if a constant is >= a Box object
int operator>=(const double& value, const Box& aBox)
{ return value >= aBox.Volume(); }

// Function for testing if a constant is <= Box object
int operator<=(const double& value, const Box& aBox)
{ return value <= aBox.Volume(); }

// Function for testing if Box object is >= a constant
int operator>=(const Box& aBox, const double& value)
{ return value <= aBox; }

// Function for testing if Box object is <= a constant
int operator<=(const Box& aBox, const double& value)
{ return value >= aBox; }

// Function for testing if a constant is == Box object
int operator==(const double& value, const Box& aBox)
{ return value == aBox.Volume(); }

// Function for testing if Box object is == a constant
int operator==(const Box& aBox, const double& value)
{ return value == aBox; }

// Box multiply operator n*aBox
Box operator*(int n, const Box aBox)
{ return aBox * n; }

// Operator to return the free volume in a packed Box
double operator%(const Box& aBox, const Box& bBox)
{ return aBox.Volume() - (aBox/bBox) * bBox.Volume(); }

We have an #include directive for Box.h because the functions refer to the Box class. We can add this file to the project by right clicking in the editor window and selecting the project name Ex7_08 that appears alongside the Insert File into Project menu option in the pop-up. When you have completed this, the Wizard Bar will show the global functions in the right-hand drop-down list box, and you can move between function definitions by selecting from this list. You have seen all these function definitions earlier in the chapter, so we won't discuss their implementations again. When we want to use them in another .cpp file, we'll need to be sure that we declare all the functions so the compiler will recognize them. We can achieve this by putting a set of declarations in a header file:

// BoxOperators.h - Declarations for global box operators
int operator>(const double& value, const Box& aBox);
int operator<(const double& value, const Box& aBox);
int operator>(const Box& aBox, const double& value);
int operator<(const Box& aBox, const double& value);
int operator>=(const double& value, const Box& aBox);
int operator<=(const double& value, const Box& aBox);
int operator>=(const Box& aBox, const double& value);
int operator<=(const Box& aBox, const double& value);
int operator==(const double& value, const Box& aBox);
int operator==(const Box& aBox, const double& value);
Box operator*(int n, const Box aBox);
double operator%(const Box& aBox, const Box& bBox);

Once you've saved this file in the project directory, you can add the file to the project by right-clicking in the editor window and selecting the option from the pop-up menu. This will ensure that the Visual C++ displays the file as part of the project in FileView immediately. If you don't add the file to the project, Visual C++ treats it as an external dependency, which it will not keep track of automatically. Such files will appear in an External Dependencies sub-folder of the files folder in FileView after you compile the program for the first time.

We're now ready to start applying these functions, along with the Box class, to a specific problem in the world of boxes.

Using Our Box Class

Let's suppose that we need to package candies. They are on the big side, real jaw breakers, occupying an envelope 1.5 inches long by 1 inch wide by 1 inch high. We have access to a standard candy box that is 4.5 inches by 7 inches by 2 inches, and we want to know how many candies fit in the box so that we can set the price. We also have a standard carton that is 2 feet 6 inches long, by 18 inches wide and 18 inches deep, and we want to know how many boxes of candy it can hold and how much space we're wasting.

In case the standard candy box isn't a good solution, we would also like to know what custom candy box would be suitable. We know that we can get a good price on boxes with a length from 3 inches to 7 inches, a breadth from 3 inches to 5 inches and a height from 1 inch to 2.5 inches, where each dimension can vary in steps of half an inch. We also know that we need to have at least 30 candies in a box, because this is the minimum quantity consumed by our largest customers at a sitting. Also, the candy box should not have empty space, because the complaints from customers who think they are being cheated goes up. Further, ideally we want to pack the standard carton completely so the candies don't rattle around. We don't want to be too stringent about this, so let's say we have no wasted space if the free space in the packed carton is less than the volume of a single candy box.

With our Box class, the problem becomes almost trivial; the solution is represented by the following main() function. Create a new text file by clicking on the new file button, then type in the code shown here and add the file to the project as Ex7_08.cpp, just as you did with the previous .cpp file:

// Ex7_08.cpp
// A sample packaging problem
#include <iostream>
#include "Box.h"
#include "BoxOperators.h"
using namespace std;

int main(void)
{
   Box Candy(1.5, 1.0, 1.0);             // Candy definition
   Box CandyBox(7.0, 4.5, 2.0);          // Candy box definition
   Box Carton(30.0, 18.0, 18.0);         // Carton definition

   // Calculate candies per candy box
   int NumCandies = CandyBox/Candy;

   // Calculate candy boxes per carton
   int NumBoxes = Carton/CandyBox;

   // Calculate wasted carton space
   double space = Carton%CandyBox;

   cout << endl
        << "There are " << NumCandies
        << " candies per candy box"
        << endl
        << "For the standard boxes there are " << NumBoxes
        << " candy boxes per carton " << endl << "with "
        << space << " cubic inches wasted.";

   cout << endl << endl
        << "CUSTOM CANDY BOX ANALYSIS (No Waste)";

   // Try the whole range of custom candy boxes
   for(double length = 3.0; length <= 7.5; length += 0.5)
      for(double breadth = 3.0; breadth <= 5.0; breadth += 0.5)
         for(double height = 1.0; height <= 2.5; height += 0.5)
         {
            // Create new box each cycle
            Box TryBox(length, breadth, height);

            if(Carton%TryBox < TryBox.Volume() &&
                      TryBox%Candy == 0.0 && TryBox/Candy >= 30)
               cout << endl << endl
                    << "TryBox L = " << TryBox.GetLength()
                    << " B = " << TryBox.GetBreadth()
                    << " H = " << TryBox.GetHeight()
                    << endl
                    << "TryBox contains "
                    << TryBox/Candy << " candies"
                    << " and a carton contains " << Carton/TryBox
                    << " candy boxes.";
         }
   cout << endl;
   return 0;
}

We should first look at how our program is structured. We've divided it into a number of files, which is common when writing in C++. You should be able to see them if you look at the FileView:

The file Ex7_08.cpp contains our function main() and a #include directive for the file BoxOperators.h, which contains the prototypes for the functions in BoxOperators.cpp (which aren't class members). It also has a #include directive for the definition of the class Box in Box.h. So, a C++ console program is usually divided into a number of files that each fall into one of three basic categories:

  • .h files containing library #include commands, global constants and variables, class definitions and function prototypes - in other words, everything except executable code. They also contain inline function definitions. Where a program has several class definitions, they are usually placed in separate .h files.

  • .cpp files containing the executable code for the program, plus #include commands for all the definitions required by the executable code.

  • Another .cpp file containing the function main().

All the .cpp files must be added explicitly to the project for the program. Visual C++ automatically incorporates all the files that are dependencies (the .h files), deducing these from the #include directives in the .cpp files.

The code in our function main() really doesn't need a lot of explanation - it's almost a direct expression of the definition of the problem in words, because the operators in the class interface perform problem-oriented actions on Box objects.

The solution to the question of the use of standard boxes is in the declaration statements, which also compute the answers we require as initializing values. We then output these values with some explanatory comments.

The second part of the problem is solved using the three nested for loops iterating over the possible ranges of length, breadth and height, so that we evaluate all possible combinations. We could output them all as well, but since this would involve 200 combinations, of which we might only be interested in a few, we have an if which defines the options that we're actually interested in. The if expression is only true if there's no space wasted in the carton and the current trial candy box has no wasted space and it contains at least 30 candies.

Here's the output from this program:

We have a duplicate solution due to the fact that, in the nested loop, we evaluate boxes that have a length of 5 and a breadth of 4.5, as well as boxes that have a length of 4.5 and a breadth of 5. Because our class constructor ensures that the length is not less than the breadth, these two are identical. We could include some additional logic to avoid presenting duplicates - you could treat it as a small exercise if you like.


(Organizing your Program Code)

Organizing your Program Code

In this last example, we distributed the code among several files for the first time. Not only is this common practice with C++ applications generally, but often it is essential. The sheer volume of code involved in even the simplest program necessitates dividing it into workable chunks.

As we discussed in the previous section, there are basically two kinds of source code file in a C++ program, .h files and .cpp files. This is illustrated in this diagram:

First of all, there's the executable code, which corresponds to the definitions of the functions that make up the program. Second, there are definitions of various kinds that are necessary for the executable code to compile correctly. These are global constants and variables, data types, including classes, structures, and unions and function prototypes. The executable source code is stored in files with the extension .cpp, and the definitions are stored in files with the extension .h.

From time to time, you might want to use code from existing files in a new project. In this case you only have to add the .cpp files to the project, which you can do by using the Project | Add To Project menu option, or by right-clicking in the editor window displaying a .cpp file and selecting from the pop-up menu to add the file to your project. You don't need to add .h files to your project, although you can if you want them to be shown in FileView immediately. The code from .h files will be added at the beginning of the .cpp files that require them as a result of the #include directives that you specify. You need #include directives for header files containing standard library functions and other standard definitions, as well as for your own header files. Visual C++ automatically keeps track of all these files, and enables you to view them in FileView. As you saw in the last example, you can also view the class definitions and global constants and variables in ClassView.

Naming Program Files

As we've already said, for classes of any complexity, it's usual to store the class definition in a .h file with a filename based on the class name, and to store the implementation of the function members of the class that are defined outside the class definition in a .cpp file with the same name. On this basis, the definition of our Box class appeared in a file with the name Box.h. Similarly, the class implementation was stored in the file Box.cpp. We didn't follow this convention in the earlier examples in the chapter because the examples were very short, and it was easier to reference the examples with names derived from the chapter number and the sequence number of the example within the chapter.

Segmenting a C++ program into .h and .cpp files is a very convenient approach, as it makes it easy for you to find the definition or implementation of any class, particularly if you're working in a development environment that doesn't have all the tools that Visual C++ provides. As long as you know the class name, you can go directly to the file you want. This isn't a rigid rule, however. It's sometimes useful to group the definitions of a set of closely-related classes together in a single file and assemble their implementations similarly. However you choose to structure your files, the ClassView will still display all the individual classes, as well as all the members of each class, as you can see here:

Here, you can see the details of the classes and globals for the last example. Double-clicking on any of the entries in the tree will take you directly to the relevant source code.


(Summary)

Summary

In this chapter, we've laid the foundations for object-oriented programming, and in the next chapter, we'll complete the knowledge of classes that you will need to understand how to apply classes to your own applications.

The key points to keep in mind from this chapter are:

  • Objects are destroyed using functions called destructors. It is essential to define a destructor to destroy objects which contain members that are allocated by new, as the default constructor will not do this.

  • The compiler will supply a default copy constructor for your class if you do not define one. The default copy constructor will not deal correctly with objects of classes that have data members allocated on the free store.

  • When you define your own copy constructor, you must use a reference parameter.

  • If you do not define an assignment operator for your class, the compiler will supply a default version. As with the copy constructor, the default assignment operator will not work correctly with classes that have data members allocated on the free store.

  • It is essential that you provide a destructor, a copy constructor and an assignment operator for classes that have members allocated by new.

  • A union is a mechanism that allows two or more variables to occupy the same location in memory.

  • A struct is like a class, except that the default access specifier is public.

  • Most basic operators can be overloaded to provide actions specific to objects of a class. You should only implement operator functions for your classes that are consistent with the normal interpretation of the basic operators.

  • A class template is a pattern that you can use to create classes with the same structure, but which support different data types.

  • You can define a class template that has multiple parameters, including parameters that can assume constant values, rather than types.

  • You should put definitions for your program in .h files, and executable code - function definitions - in .cpp files. You then can incorporate .h files into your .cpp files by using #include directives.

  • By using the Visual C++ integrated development environment and the Wizard Bar, you can automate a lot of the work in accessing and modifying your program code.


(Class Inheritance)

Chapter 8 - Class Inheritance

In this chapter, we're going to look into a topic that lies at the heart of object-oriented programming - inheritance. Simply put, inheritance is the means by which you can define a new class in terms of one you already have. Inheritance comes up time and again in C++ programming, so it's important that you understand how inheritance works.

In this chapter you will learn about:

  • How inheritance fits into the idea of object-oriented programming

  • Defining a new class in terms of an existing one

  • The use of the keyword protected to define a new access specification for class members

  • How a class can be a friend to another class

  • Virtual functions and how you can use them

  • Pure virtual functions

  • Abstract classes

  • Virtual destructors and when to use them

  • Multiple inheritance

  • Dynamic casts


(Basic Ideas of OOP)

Basic Ideas of OOP

As we have seen, a class is a data type that you define to suit your own application requirements. Classes in object-oriented programming also define the objects to which your program relates. You program the solution to a problem in terms of the objects that are specific to the problem, using operations that work directly with those objects. You can define a class to represent something abstract, such as a complex number, which is a mathematical concept, or a truck, which is decidedly physical (especially if you run into one on the highway). So, as well as being a data type, a class can also be a definition of a real-world object, at least to the degree necessary to solve a given problem.

You can think of a class as defining the characteristics of a particular group of things that are specified by a common set of parameters and share a common set of operations that may be performed on them. The operations that are possible are defined by the class interface contained in the public section of the class definition. The Box class that we used in the last chapter is a good example - it defined a box in the most elementary terms.

Of course, in the real world there are many different kinds of box: there are cartons, coffins, candy boxes and cereal boxes, and you will certainly be able to come up with many others. You can differentiate boxes by the kinds of things they hold, the materials they are made of, and in a multitude of other ways, but even though there are many different kinds of box, they share some common characteristics - the essence of boxiness perhaps - and, therefore, you can still visualize them as actually being related to one another, even though they have many differentiating features. You could define a particular kind of box as having the generic characteristics of all boxes - perhaps just a length, a breadth and a height - plus some additional parameters which serve to differentiate your kind of box from the rest. You may also find that there are new things you can do with your particular kind of box that you can't do with other boxes.

Equally, some objects may be the result of combining a particular kind of box with some other kind of object: a box of candy, or a crate of beer for example. You can, of course, define one kind of box as a generic box, plus some additional characteristics, and then specify another sort of box as a further specialization of that. An example of the kinds of relationships you might define between different sorts of boxes is illustrated below.

The boxes become more specialized as you move down the diagram. In this case, we have defined three different kinds of box, based on the generic type. We also have beer crates defined as a further refinement of crates designed to hold bottles.

You could deduce from this that a good way to approximate the real world relatively well, using classes in C++, would be through the ability to define classes that are interrelated. A candy box can be considered to be a box with all the characteristics of a basic box, plus a few characteristics of its own. This precisely illustrates the relationship between classes in C++ when one class is defined based on another, and is shown in the diagram above. Let's look at how this works in practice.


(Inheritance in Classes)

Inheritance in Classes

When one class is defined based on another class (or more generally, based on several others), the former is referred to as a derived class. A derived class automatically contains all the data members of the class or classes which are used to define it and, with some restrictions, the function members as well. The class is said to inherit the data members and function members of the classes on which it is based.

The only members of a base class which are not inherited by a derived class are the destructor, the constructors and any member functions overloading the assignment operator. All other function members, together with all the data members of a base class, will be inherited by a derived class.

What is a Base Class?

A base class is any class that is used in the definition of another class. For example, when a class B is defined directly in terms of a class A, A is said to be a direct base class of B. In the previous diagram, the class Crate was a direct base class of BeerCrate. When a class such as BeerCrate is defined in terms of another class Crate, BeerCrate is said to be derived from Crate. Because Crate is itself defined in terms of the class Box, Box is said to be an indirect base class of BeerCrate. We shall see how this is expressed in the class definition in a moment. The relationship between a derived class and a base class is illustrated in the following figure:

Deriving Classes from a Base Class

Let's go back to the original Box class with public data members that we had at the beginning of the last chapter:

// Listing 8_01-01
class Box
{
   public:
      double length;
      double breadth;
      double height;
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         length = lv;
         breadth = bv;
         height = hv;
      }
};

We have included a constructor in the class so that we can initialize objects when we declare them. Suppose we now need another class CandyBox, which is the same as a Box object, but also has another data member - a pointer to a text string. We can define CandyBox as a derived class with the class Box as the base class, as follows:

// Listing 8_01-02
class CandyBox: Box
{
   public:
      char* contents;
      CandyBox(char* str= "Candy")            // Constructor
      {
         contents = new char[ strlen(str) + 1 ];
         strcpy(contents, str);
      }

      ~CandyBox()                             // Destructor
      { delete[] contents; };
};

The base class, Box, appears after the class name for the derived class CandyBox and is separated from it by a colon. In all other respects, it looks like a normal class definition. We have added the new member, contents, and, since it is a pointer to a string, we need a constructor to initialize it and a destructor to release the memory for the string. We have also put a default value for the string describing the contents of a CandyBox object in the constructor. Objects of the class CandyBox contain all the members of the base class, Box, plus the additional data member, contents.

Try It Out - Using a Derived Class

We can see how our derived class works in an example. Although we used the Wizard Bar to manage our classes in the previous chapter, we will hold off doing that for the most part in this chapter, because we are often combining fragments that we have been developing in discussion. However, if you feel comfortable using the Wizard Bar to put the examples together, it would be good experience to do it that way. Here is the code for our first example using a derived class:

// Ex8_01.cpp
// Using a derived class
#include <iostream>
#include <string>               // For strlen() and strcpy()
using namespace std;

// Insert Box definition (Listing 8_01-01)

// Insert CandyBox definition (Listing 8_01-02)

int main()
{
   Box myBox(4.0,3.0,2.0);           // Create Box object
   CandyBox myCandyBox;              // Create CandyBox objects
   CandyBox myMintBox("Wafer Thin Mints");

   cout << endl
        << "myBox occupies "
        << sizeof myBox             // Show how much memory
        << " bytes" << endl         // the objects require
        << "myCandyBox occupies " << sizeof myCandyBox
        << " bytes" << endl
        << "myMintBox occupies " << sizeof myMintBox
        << " bytes";

   cout << endl
        << "myBox length is " << myBox.length;

   myBox.length = 10.0;
   // myCandyBox.length = 10.0;  // uncomment this for an error

   cout << endl;
   return 0;
}

How It Works

After declaring a Box object and two CandyBox objects, we output the number of bytes occupied by each object. Let's look at the output:

The first line is what we would expect from our discussion in the last chapter. A Box object has three data members of type double, each of which will be 8 bytes, making 24 bytes in all. Both our CandyBox objects are the same size: 32 bytes. The length of the string doesn't affect the size of an object, as the memory for it is allocated in the free store. The 32 bytes are made up of 24 bytes for the three double members inherited from the base class Box, plus 4 bytes for the pointer member contents, which makes 28 bytes... so where did the other 4 bytes come from? This is due to the compiler aligning members at addresses that are multiples of 8 bytes. You should be able to demonstrate this by adding an extra member of type int, say, to the class CandyBox. You will find that the size of a class object is still 32 bytes.

We also output the value of the length member of the Box object myBox. Even though we have no difficulty accessing this member of the Box object, if you uncomment the following statement in the function main(),

// myCandyBox.length = 10.0;  // uncomment this for an error

then the program will no longer compile. The compiler will generate a message,

error C2248: 'length': cannot access public member declared in class 'Box'

which means that the length member from the base class is not accessible. In the derived class, the member length has become private.

The reason for this is that there is a default access specifier of private for a base class - it's as if the first line of our class definition had been:

class CandyBox: private Box

There always has to be an access specification for a base class, which will determine the status of the inherited members in the derived class. Omitting an access specification when specifying a base class causes the compiler to assume that it's private. If we change the definition of the class CandyBox to the following,

class CandyBox: public Box
{
   // Contents the same as in Listing 8_01-02
};

then the member length in the derived class will be inherited as public and will be accessible in the function main(). With the access specifier public for the base class, all the inherited members originally specified as public in the base class will have the same access level in the derived class.


(Access Control Under Inheritance)

Access Control Under Inheritance

The whole question of the access of inherited members in a derived class needs to be looked at more closely. Firstly, we should consider the private members of a base class in a derived class.

There was a good reason for choosing the version of the class Box with public data members, rather than the later, more secure version with private data members that we looked at. The reason was that although private data members of a base class are also members of a derived class, they remain private to the base class in the derived class, and this means that member functions added to the derived class cannot access them. They are only accessible in the derived class through function members of the base class that are not in the private section of the base class. You can demonstrate this very easily by changing all the Box class data members to private and putting a function Volume() in the derived class CandyBox, so that the class definitions become as follows:

// Version of the classes that will not compile
class Box
{
   public:
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         length = lv;
         breadth = bv;
         height = hv;
      }

   private:
      double length;
      double breadth;
      double height;
};

class CandyBox: public Box
{
   public:
      char* contents;

      // Function to calculate the volume of a CandyBox object
      double Volume()         // Error - members not accessible
      { return length*breadth*height; }

      CandyBox(char* str= "Candy")   // Constructor
      {
         contents = new char[ strlen(str) + 1 ];
         strcpy(contents, str);
      }

      ~CandyBox()                    // Destructor
      { delete[] contents; }
};

A program using these classes will not compile. The function Volume() in the class CandyBox attempts to access the private members of the base class, which is not legal.

Try It Out - Accessing Private Members of the Base Class

It is, however, legal to use the Volume() function in the base class, so if you move the definition of the function Volume() to the public section of the base class, Box, not only will the program compile but you can use the function to obtain the volume of a CandyBox object:

// Ex8_02.cpp
// Using a function inherited from a base class
#include <iostream>          // For stream I/O
#include <string>            // For strlen() and strcpy()
using namespace std;

class Box
{
   public:
      //Function to calculate the volume of a Box object
      double Volume()
      { return length*breadth*height; }

      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         length = lv;
         breadth = bv;
         height = hv;
      }

   private:
      double length;
      double breadth;
      double height;
};

class CandyBox: public Box
{
   // Contents the same as in Listing 8_01-02
};

int main()
{
   Box myBox(4.0,3.0,2.0);            // Create Box object
   CandyBox myCandyBox;               // Create CandyBox objects
   CandyBox myMintBox("Wafer Thin Mints");

   cout << endl
        << "myBox occupies "
        << sizeof myBox               // Show how much memory
        << " bytes" << endl           // the objects require
        << "myCandyBox occupies " << sizeof myCandyBox
        << " bytes" << endl
        << "myMintBox occupies " << sizeof myMintBox
        << " bytes";

   cout << endl             // Get volume of a CandyBox object
        << "myMintBox volume is " << myMintBox.Volume();

   cout << endl;
   return 0;
}

How It Works

This example will produce the following output:

The interesting additional output is the last line. This shows the value produced by the function Volume(), which is now in the public section of the base class. Within the derived class, it operates on the members of the derived class that are inherited from the base. It is a full member of the derived class, so it can be used freely with objects of the derived class.

The value for the volume of the derived class object is 1 because, in creating the CandyBox object, the default constructor Box() was called first to create the base class part of the object, and this sets default Box dimensions to 1.

Constructor Operation in a Derived Class

The constructor for the base part of the derived class was called automatically in the last example, but this doesn't have to be the case. We can arrange to call a particular base class constructor from the derived class constructor. This will enable us to initialize the base class data members with a constructor other than the default, or indeed to choose one or other base class constructor, depending on the data supplied to the derived class constructor.

Try It Out - Calling Constructors

We can demonstrate this in action using a modified version of the last example. To make the class usable, we really need to provide a constructor for the derived class which allows you to specify the dimensions of the object. We can produce an additional constructor in the derived class to do this, and call the base class constructor explicitly to set the values of the data members inherited from the base class.

// Ex8_03.cpp
// Calling a base constructor from a derived class constructor
#include <iostream>          // For stream I/O
#include <string>            // For strlen() and strcpy()
using namespace std;

class Box
{
   public:
      //Function to calculate the volume of a Box object
      double Volume()
      { return length*breadth*height; }

      // Base class constructor
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         cout << endl << "Box constructor called";
         length = lv;
         breadth = bv;
         height = hv;
      }

   private:
      double length;
      double breadth;
      double height;
};

class CandyBox: public Box
{
   public:
      char* contents;
      // Constructor to set dimensions and contents
      // with explicit call of Box constructor
      CandyBox(double lv, double bv, double hv,
                             char* str= "Candy"):Box(lv, bv, hv)
      {
          cout << endl <<"CandyBox constructor2 called";
          contents = new char[ strlen(str) + 1 ];
          strcpy(contents, str);
      }

      // Constructor to set contents
      // calls default Box constructor automatically
      CandyBox(char* str= "Candy")
      {
         cout << endl << "CandyBox constructor1 called";
         contents = new char[ strlen(str) + 1 ];
         strcpy(contents, str);
      }

      ~CandyBox()                             // Destructor
      { delete[] contents; }
};

int main()
{
   Box myBox(4.0,3.0,2.0);
   CandyBox myCandyBox;
   CandyBox myMintBox(1.0,2.0,3.0,"Wafer Thin Mints");

   cout << endl
        << "myBox occupies "
        << sizeof  myBox               // Show how much memory
        << " bytes" << endl            // the objects require
        << "myCandyBox occupies " << sizeof myCandyBox
        << " bytes" << endl
        << "myMintBox occupies " << sizeof myMintBox
        << " bytes";

   cout << endl
        << "myMintBox volume is "              // Get volume of a
        << myMintBox.Volume();                 // CandyBox object

   cout << endl;
   return 0;
}

How It Works

As well as adding the additional constructor in the derived class, we have put an output statement in each constructor so we will know when either gets called. The explicit call of the constructor for the Box class appears after a colon in the function header of the derived class constructor. You will have perhaps noticed that the notation is exactly the same as that used for initializing members in a constructor, which we saw in the last chapter:

// Initializing members
Box(double lv=1.0, double bv=1.0, double hv=1.0):length(lv),
                                                 breadth(bv),
                                                 height(hv)
{
...
}

// Calling the base class constructor
CandyBox(double lv, double bv, double hv, char* str= "Candy"):Box(lv,bv,hv)
{
...
}

This is perfectly consistent with what we are doing here, since we are essentially initializing a Box sub-object of the derived class object. In the first case, we are explicitly calling the default constructors for the double types length, breadth and height in the initialization list. In the second instance, we are calling the constructor for Box. This causes the specific Box constructor we have chosen to be called before the CandyBox constructor is executed.

If you build and run this example, it will produce the output shown below:

The calls to the constructors are explained in the table below:

Screen output Object being constructed
Box constructor called
myBox
Box constructor called
myCandyBox
CandyBox constructor1 called
myCandyBox
Box constructor called
myMintBox
CandyBox constructor2 called
myMintBox

The first line of output is due to the Box class constructor call, originating from the declaration of the Box object, myBox. The second line of output arises from the automatic call of the base class constructor caused by the declaration of the CandyBox object myCandyBox.

Notice how the base class constructor is always called before the derived class constructor.

The following line is due to our version of the default derived class constructor being called for the object myCandyBox. This constructor is invoked because the object is not initialized. The fourth line of output arises from the explicit identification of the Box class constructor to be called in our new constructor for CandyBox objects. The argument values specified for the dimensions of the CandyBox object are passed to the base class constructor. Next comes the output from the new derived class constructor itself, so constructors are again called for the base class first, followed by the derived class.

The last line shows that the initialization of the base part of the object myMintBox is working as it should, with the private members having been initialized by the Box class constructor.

Having the private members of a base class only accessible to function members of the base class isn't always convenient. There will be many instances where we want to have private members of a base class that can be accessed from within the derived class. As you will surely have anticipated by now, C++ provides a way to do this.

Declaring Protected Members of a Class

In addition to the public and private access specifiers for members of a class, you can also declare members of a class as protected. Within the class, the keyword protected has the same effect as the keyword private: members of a class that are protected can only be accessed by member functions of the class, and by friend functions of the class (also by member functions of a class that is declared as a friend of the class - we will look into friend classes later in this chapter). Using the keyword protected, we could redefine our class Box as follows:

// Listing 8_04-01
class Box
{
   public:
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         cout << endl << "Box constructor called";
         length = lv;
         breadth = bv;
         height = hv;
      }

      // Box destructor - just to track calls
      ~Box()
      { cout << "Box destructor called" << endl; }

   protected:
      double length;
      double breadth;
      double height;
};

Now the data members are still effectively private, in that they can't be accessed by ordinary global functions, but they'll still be accessible to member functions of a derived class.

Try It Out - Using Protected Members

We can demonstrate the use of protected data members by using this version of the class Box to derive a new version of the class CandyBox, which will access the members of the base class through its own member function, Volume().

// Ex8_04.cpp
// Using the protected access specifier
#include <iostream>             // For stream I/O
#include <string>               // For strlen() and strcpy()
using namespace std;

// Insert Box class definition here (Listing 8_04-01)

class CandyBox: public Box
{
   public:
      char* contents;

      // Derived class function to calculate volume
      double Volume()
      { return length*breadth*height; }

      // Constructor to set dimensions and contents
      // with explicit call of Box constructor
      CandyBox(double lv, double bv, double hv,
                               char* str= "Candy"):Box(lv,bv,hv)
      {
         cout << endl <<"CandyBox constructor2 called";
         contents = new char[ strlen(str) + 1 ];
         strcpy(contents, str);
      }

      // Constructor to set contents
      // calls default Box constructor automatically
      CandyBox(char* str= "Candy")            // Constructor
      {
         cout << endl << "CandyBox constructor1 called";
         contents = new char[ strlen(str) + 1 ];
         strcpy(contents, str);
      }

      ~CandyBox()                             // Destructor
      {
         cout << "CandyBox destructor called" << endl;
         delete[] contents;
      }
};

int main()
{
   CandyBox myCandyBox;
   CandyBox myToffeeBox(2,3,4,"Stickjaw Toffee");
   cout << endl
        << "myCandyBox volume is " << myCandyBox.Volume()
        << endl
        << "myToffeeBox volume is " << myToffeeBox.Volume();
   cout << endl;

   // cout << myToffeeBox.length; // Uncomment for an error

   cout << endl;
   return 0;
}

How It Works

In this example, the volumes of the two CandyBox objects are calculated by invoking the function Volume(), which is a member of the derived class. This function accesses the inherited members length, breadth, and height to produce the result. The members were declared as protected in the base class and remain protected in the derived class. The program produces the output shown below:

This shows that the volume is being calculated properly for both CandyBox objects. The first object has the default dimensions produced by calling the default Box constructor, so the volume is 1, and the second object has the dimensions defined as initial values in its declaration.

The output also shows the sequence of constructor and destructor calls.

Destructors for a derived class object are called in the reverse order to the constructors for the object. This is a general rule that always applies. Constructors are invoked starting with the base class constructor and then the derived class constructor, whereas the destructor for the derived class is called first when an object is destroyed, followed by the base class destructor.

You can demonstrate that the protected members of the base class remain protected in the derived class by uncommenting the statement preceding the return statement in the function main(). If you do this, you will get the following error message from the compiler,

error C2248: 'length': cannot access protected member declared in class 'Box'

which indicates quite clearly that the member length is inaccessible.

The Access Level of Inherited Class Members

We know that if we have no access specifier for the base class in the definition of a derived class, the default specification is private. This has the effect of causing the inherited public and protected members of the base class to become private in the derived class. The private members of the base class remain private to the base and therefore inaccessible to member functions of the derived class. In fact, they remain private to the base class regardless of how the base class is specified in the derived class definition.

We have also used public as the specifier for a base class. This leaves the members of the base class with the same access level in the derived class as they had in the base, so public members remain public and protected members remain protected.

The last possibility is to declare a base class as protected. This has the effect of making the inherited public members of the base protected in the derived class. The protected (and private) inherited members retain their original access level in the derived class.

This is summarized in the following illustration:

This may look a little complicated, but you can reduce it to the following three points about the inherited members of a derived class:

  • Members of a base class that are declared as private are never accessible in a derived class

  • Defining a base class as public doesn't change the access level of its members in the derived class

  • Defining a base class as protected changes its public members to protected in the derived class

Being able to change the access level of inherited members in a derived class gives you a degree of flexibility, but remember that you can only make the access level more stringent. In no way can you relax the level specified in the base class, which suggests that your base classes need to have public members if you want to be able to vary the access level in derived classes. This may seem to run contrary to the idea of encapsulating data in a class in order to protect it from unauthorized access, but, as we shall see, it will often be the case that we construct base classes in such a manner that their only purpose is to act as a base for other classes, and they aren't intended to be used for instantiating objects in their own right.


(The Copy Constructor in a Derived Class)

The Copy Constructor in a Derived Class

You will remember that the copy constructor is called automatically when you declare an object which is initialized with an object of the same class. Look at these statements:

Box myBox(2.0, 3.0, 4.0);    // Calls constructor
Box CopyBox(myBox);          // Calls copy constructor

The first will call the constructor, accepting three double arguments, and the second will call the copy constructor. If you don't supply your own copy constructor, the compiler will supply one that copies the initializing object, member by member, to the corresponding members of the new object. So that we can see what is going on during execution, let's add our own version of a copy constructor to the class Box. We can then use this class as a base for defining the class CandyBox.

// Listing 8_05-01
class Box              // Base class definition
{
   public:
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
      {
         cout << endl << "Box constructor called";
         length = lv;
         breadth = bv;
         height = hv;
      }

      // Copy constructor
      Box(const Box& initB)
      {
         cout << endl << "Box copy constructor called";
         length = initB.length;
         breadth = initB.breadth;
         height = initB.height;
      }

      // Box destructor - just to track calls
      ~Box()
      { cout << "Box destructor called" << endl; }

   protected:
      double length;
      double breadth;
      double height;
};

You'll also recall that the copy constructor needs to have its parameter specified as a reference in order to avoid an infinite number of calls to itself, caused by the need to copy an argument that is transferred by value. When the copy constructor in our example is invoked, it will output a message to the screen, so we'll be able to see from the output when this is happening.

We will use the CandyBox class from Ex8_04.cpp, shown again here:

// Listing 8_05-02
class CandyBox: public Box
{
   public:
      char* contents;

      // Derived class function to calculate volume
      double Volume()
      { return length*breadth*height; }

      // Constructor to set dimensions and contents
      // with explicit call of Box constructor
      CandyBox(double lv, double bv, double hv,
                              char* str= "Candy"):Box(lv,bv,hv)
      {
         cout << endl <<"CandyBox constructor2 called";
         contents = new char[ strlen(str) + 1 ];
         strcpy(contents, str);
      }

      // Constructor to set contents
      // calls default Box constructor automatically
      CandyBox(char* str= "Candy")            // Constructor
      {
         cout << endl << "CandyBox constructor1 called";
         contents = new char[ strlen(str) + 1 ];
         strcpy(contents, str);
      }

      ~CandyBox()                             // Destructor
      {
         cout << "CandyBox destructor called" << endl;
         delete[] contents;
      }
};

This doesn't have a copy constructor added yet, so we'll rely on the compiler-generated version.

Try It Out - The Copy Constructor in Derived Classes

We can exercise the copy constructor that we have just defined with the following example:

// Ex8_05.cpp
// Using a derived class copy constructor
#include <iostream>          // For stream I/O
#include <string>            // For strlen() and strcpy()
using namespace std;

// Insert Box class definition here (Listing 8_05-01)

// Insert CandyBox class definition here (Listing 8_05-02)

int main()
{
   // Declare and initialize
   CandyBox ChocBox(2.0, 3.0, 4.0, "Chockies");
   // Use copy constructor
   CandyBox ChocolateBox(ChocBox);

   cout << endl
        << "Volume of ChocBox is " << ChocBox.Volume()
        << endl
        << "Volume of ChocolateBox is " << ChocolateBox.Volume()
        << endl;

   return 0;
}

How It Works (or why it doesn't)

When you run this example, in addition to the expected output, you'll see the following message:

Press Abort to clear the dialog and you'll see the output in the console window that you might expect. The output shows that the compiler-generated copy constructor for the derived class automatically called the copy constructor for the base class.

However, as you've probably realized, all is not as it should be. In this particular case, the compiler-generated copy constructor causes problems because the memory pointed to by the contents member of the derived class in the second object declared will point to the same memory as the one in the first object. When one object is destroyed (when it goes out of scope at the end of main()), it releases the memory occupied by the text. When the second object is destroyed, the destructor attempts to release some memory that has already been freed by the destructor call for the previous object.

The way to fix this is to supply a copy constructor for the derived class that will allocate some additional memory for the new object.

Try It Out - Fixing the Copy Constructor Problem

We can do this by adding the following code for the copy constructor to the public section of the derived class:

// Derived class copy constructor
CandyBox(const CandyBox& initCB)
{
   cout << endl << "CandyBox copy constructor called";
   // Get new memory
   contents = new char[ strlen(initCB.contents) + 1 ];
   // Copy string
   strcpy(contents, initCB.contents);
}

We can now run this new version of the last example with the same function main() to see how our copy constructor works.

How It Works

Now when we run the example, it behaves rather better and produces the output shown in the screen below:

However, there is still something wrong. The third line of output shows that the default constructor for the Box part of the object ChocolateBox is called, rather than the copy constructor. As a consequence, the object has the default dimensions rather than the dimensions of the initializing object, so the volume is incorrect. The reason for this is that when you write a constructor for an object of a derived class, you are responsible for ensuring that the members of the derived class object are properly initialized. This includes the inherited members.

The fix for this is to call the copy constructor for the base part of the class in the initialization list for the copy constructor for the CandyBox class. The copy constructor would then become:

// Derived class copy constructor
CandyBox(const CandyBox& initCB): Box(initCB)
{
   cout << endl << "CandyBox copy constructor called";
   // Get new memory
   contents = new char[ strlen(initCB.contents) + 1 ];
   // Copy string
   strcpy(contents, initCB.contents);
}

Now, the Box class copy constructor is called with the initCB object. Only the base part of the object will be passed to it, so everything will work out. If you modify the last example by adding the base copy constructor call, the output will be as shown:

The output shows that all the constructors and destructors are called in the correct sequence, and that the copy constructor for the Box part of ChocolateBox is called before the CandyBox copy constructor. The volume of the object ChocolateBox of the derived class is now the same as that of its initializing object, which is as it should be.

We have, therefore, another golden rule to remember:

If you write any kind of constructor for a derived class, you are responsible for the initialization of all members of the derived class object, including all its inherited members.


(Class Members as friends)

Class Members as friends

We have already seen how a function could be declared as a friend of a class. This gave the friend function the privilege of free access to any of the class members. Of course, there is no reason why a friend function cannot be a member of another class.

Suppose we define a class Bottle to represent a bottle:

class Bottle
{
   private:
      double height;       // Bottle height
      double diameter;     // Bottle diameter

   public:
      Bottle(double height, double diameter)
      {
         height = height;
         diameter = diameter;
      }
};

We now need a class to represent the packaging for a dozen bottles, that will automatically have custom dimensions to accommodate a particular kind of bottle. We could define this as:

class Carton
{
   private:
      double length;        // Carton length
      double breadth;       // Carton breadth
      double height;        // Carton height

   public:
      Carton(Bottle aBottle)
      {
         height = aBottle.height;           // Bottle height
         length = 4.0*aBottle.diameter;     // Four rows of ...
         breadth = 3.0*aBottle.diameter;    // ...three bottles
      }
};

The constructor here sets the height to be the same as that of the bottle it is to accommodate, and the length and breadth are set based on the diameter of the bottle so that twelve will fit in the box.

As you know by now, this won't work. The data members of the Bottle class are private, so the Carton constructor cannot access them. As you also know, a friend declaration in the Bottle class should fix this:

class Bottle
{
   private:
      double height;       // Bottle height
      double diameter;   // Bottle diameter

   public:
      Bottle(double height, double diameter)
      {
         height = height;
         diameter = diameter;
      }

      // Let the carton constructor in
      friend Carton::Carton(Bottle aBottle);
};

When we declare a member function as a friend of another class, we need to put the class name and the scope resolution operator with the friend function name to identify it. For this to compile correctly, the compiler needs to have information about the Carton class constructor, so you would need to put an #include statement for the file containing the Carton class definition before the definition of the Bottle class.

Friend Classes

You can also allow all the function members of one class to have access to all the data members of another by declaring it as a friend class. We could define the Carton class as a friend of the Bottle class by writing a friend declaration in the Bottle class definition:

friend Carton;

All function members of the Carton class will now have free access to all the data members of the Bottle class.

Limitations on Class Friendship

Class friendship is not reciprocated. Making the Carton class a friend of the Bottle class does not mean that the Bottle class is a friend of the Carton class. If you want this to be so you must add a friend declaration for the Bottle class to the Carton class.

Class friendship is also not inherited. If you define another class with Bottle as a base, members of the Carton class will not have access to its data members, not even those that are inherited from Bottle.


(Virtual Functions)

Virtual Functions

We need to look more closely at the behavior of inherited member functions and their relationship with derived class member functions. Let's add a function to the class Box to output the volume of a Box object. The simplified class would then become:

// Listing 8_06-01
class Box            // Base class
{
   public:
      // Function to show the volume of an object
      void ShowVolume()
      {
         cout << endl
              << "Box usable volume is " << Volume();
      }

      // Function to calculate the volume of a Box object
      double Volume()
      { return length*breadth*height; }

      // Constructor
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
                        :length(lv), breadth(bv), height(hv) {}

   protected:
      double length;
      double breadth;
      double height;
};

Now we can output the usable volume of a Box object just by calling the ShowVolume() function of any object for which we require it. The constructor sets the data member values in the initialization list, so no statements are necessary in the body of the function. The data members are as before and are specified as protected, so they will be accessible to the member functions of any derived class.

Now, let's suppose we want to derive a class for a different kind of box called GlassBox, to hold glassware. The contents are fragile, and because packing material is added to protect them, the capacity of the box is less than the capacity of a basic Box object. We therefore need a different Volume() function to account for this, so we add it to the derived class:

// Listing 8_06-02
class GlassBox: public Box     // Derived class
{
   public:
      // Function to calculate volume of a GlassBox
      // allowing 15% for packing

      double Volume()
      { return 0.85*length*breadth*height; }

      // Constructor
      GlassBox(double lv, double bv, double hv): Box(lv,bv,hv){}
};

There could conceivably be other additional members of the derived class, but we'll keep it simple and concentrate on how the inherited functions work for the moment. The constructor for the derived class objects just calls the base class constructor in its initialization list to set the data member values. No statements are necessary in its body. We've included a new version of the function Volume() to replace the version from the base class, the idea being that we can get the inherited function ShowVolume() to call the derived class version of the member function Volume() when we call it for an object of the class GlassBox.

Try It Out - Using an Inherited Function

Now we should see how our derived class works in practice. We can try this out very simply by creating an object of the base class and an object of the derived class with the same dimensions, and then verifying that the correct volumes are being calculated. The main() function to do this would be as follows:

// Ex8_06.cpp
// Behavior of inherited functions in a derived class
#include <iostream>
using namespace std;

// Insert Box class definition (Listing 8_06-01)

// Insert GlassBox class definition (Listing 8_06-02)

int main()
{
   Box myBox(2.0, 3.0, 4.0);           // Declare a base box
   GlassBox myGlassBox(2.0, 3.0, 4.0); // Declare derived box

   myBox.ShowVolume();          // Display volume of base box
   myGlassBox.ShowVolume();     // Display volume of derived box

   cout << endl;
   return 0;
}

How It Works

If you run this example, it will produce the following output:

This isn't only dull and repetitive, it's also disastrous. It isn't working the way we want at all, and the only interesting thing about it is why. Evidently, the fact that the second call is for an object of the derived class GlassBox is not being taken into account. We can see this from the incorrect result in the output. The volume of a GlassBox object should definitely be less than that of a basic Box with the same dimensions.

The reason for the incorrect output is that the call of the function Volume() in the function ShowVolume() is being set once and for all by the compiler as the version defined in the base class. This is called static resolution of the function call, or static linkage - the function call is fixed before the program is executed. This is also sometimes called early binding because the particular Volume() function chosen is bound to the call from the function ShowVolume() during the compilation of the program.

The function Volume() here in the derived class actually hides the base class version from the view of derived class functions. If you wanted to call the base version of Volume() from a derived class function, you would need to use the scope resolution operator to refer to the function as Box::Volume().

What we were hoping for in this example was that the question of which Volume() function call to use in any given instance would be resolved when the program was executed. This sort of operation is referred to as dynamic linkage, or late binding. We want the actual version of the function Volume() called by ShowVolume() to be determined by the kind of object being processed, and not arbitrarily fixed by the compiler before the program is executed.

No doubt you'll be less than astonished that C++ does, in fact, provide us with a way to do this, since this whole discussion would have been futile otherwise! We need to use something called a virtual function.

What's a Virtual Function?

A virtual function is a function in a base class that is declared using the keyword virtual. Defining in a base class a virtual function, with another version in a derived class, signals to the compiler that we don't want static linkage for this function. What we do want is the selection of the function to be called at any given point in the program to be based on the kind of object for which it is called.

Try It Out - Fixing the GlassBox

To make our example work as we originally hoped, we just need to add the keyword virtual to the definitions of the Volume() functions:

// Ex8_07.cpp (based on Ex8_06.cpp)
// Using a virtual function
#include <iostream>
using namespace std;

class Box            // Base class
{
   public:
...
      // Function to calculate the volume of a Box object
      virtual double Volume()
      { return length*breadth*height; }
...
};

class GlassBox: public Box
{
   public:
...
      // Function to calculate volume of a GlassBox
      // allowing 15% for packing
      virtual double Volume()
      { return 0.85*length*breadth*height; }
...
};

int main()
{
...
}

How It Works

If you run this version of the program with just the little word virtual added to the definitions of Volume(), it will produce this output:

This is now clearly doing what we wanted in the first place. The first call to the function ShowVolume() with the Box object, myBox, calls the base version of Volume(). The second call with the GlassBox object, myGlassBox, calls the version defined in the derived class.

Note that, although we have put the keyword virtual in the derived class definition of the function Volume(), it's not essential to do so. The definition of the base version of the function as virtual is sufficient. However, I recommend that you do specify the keyword for virtual functions in derived classes, since it makes it clear to anyone reading the derived class definition that they are virtual functions and that they will be selected dynamically.

In order for a function to behave as virtual, it must have the same name, parameter list, and return type in any derived class as the function has in the base class. If you try to use different parameters or return types, the virtual function mechanism won't work. The function will operate with static linkage established and fixed at compile time.

The operation of virtual functions is an extraordinarily powerful mechanism. You may have heard the term polymorphism in relation to object-oriented programming, and this refers to the virtual function capability. Something that is polymorphic can appear in different guises, like a werewolf, or Dr. Jekyll, or a politician before and after an election, for example. Calling a virtual function will produce different effects depending on the kind of object for which it is being called.

Using Pointers to Class Objects

Using pointers with objects of a base class and of a derived class is a very important technique. A pointer to a base class object can be assigned the address of a derived class object as well as that of the base. We can thus use a pointer of the type 'pointer to base' to obtain different behavior with virtual functions, depending on what kind of object the pointer is pointing to. We can see how this works more clearly by looking at an example.

Try It Out - Pointers to Base and Derived Classes

Let's use the same classes as in the previous example, but make a small modification to the function main() so that it uses a pointer to a base class object.

// Ex8_08.cpp
// Using a base class pointer to call a virtual function
#include <iostream>
using namespace std;

class Box        // Base class
{
   public:
      // Function to show the volume of an object
      void ShowVolume()
      {
         cout << endl
              << "Box usable volume is " << Volume();
      }

      // Function to calculate the volume of a Box object
      virtual double Volume()
      { return length*breadth*height; }

      // Constructor
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
                         :length(lv), breadth(bv), height(hv) {}

   protected:
      double length;
      double breadth;
      double height;
};

class GlassBox: public Box     // Derived class
{
   public:
      // Function to calculate volume of a GlassBox
      // allowing 15% for packing
      virtual double Volume()
      { return 0.85*length*breadth*height; }

      // Constructor
      GlassBox(double lv, double bv, double hv): Box(lv,bv,hv){}
};

int main()
{
   Box myBox(2.0, 3.0, 4.0);         // Declare a base box
   GlassBox myGlassBox(2.0,3.0,4.0); // Declare derived box

   Box* pBox = 0;      // Declare a pointer to base class objects
   pBox = &myBox;      // Set pointer to address of base object

   pBox->ShowVolume(); // Display volume of base box
   pBox = &myGlassBox; // Set pointer to derived class object
   pBox->ShowVolume(); // Display volume of derived box

   cout << endl;
   return 0;
}

How It Works

The classes are the same as in example Ex8_07.cpp, but the function main() has been altered to use a pointer to call the function ShowVolume(). Because we are using a pointer, we use the indirect member selection operator, ->, to call the function. The function ShowVolume() is called twice, and both calls use the same pointer to base class objects, pBox. On the first occasion, the pointer contains the address of the base object, myBox, and on the occasion of the second call, it contains the address of the derived class object, myGlassBox.

The output produced is as follows:

This output is exactly the same as that from the previous example, where we used explicit objects in the function call.

We can conclude from this example that the virtual function mechanism works just as well through a pointer to a base class, with the specific function being selected based on the type of object being pointed to. This is illustrated in the following figure.

This means that, even when we don't know the precise type of the object pointed to by a base class pointer in a program (when a pointer is passed to a function as an argument, for example), the virtual function mechanism will ensure that the correct function is called. This is an extraordinarily powerful capability, so make sure you understand it. Polymorphism is a fundamental mechanism in C++ that you will find yourself using again and again.

Using References With Virtual Functions

If you define a function with a reference to a base class as a parameter, you can pass an object of a derived class to it as an argument. When your function executes, the appropriate virtual function for the object passed will be selected automatically. We could show this happening by modifying the function main() in the last example to call a function that has a reference as a parameter.

Try It Out - Using References with Virtual Functions

Let's move the call to ShowVolume() to a separate function, and call that separate function from main():

// Ex8_09.cpp
// Using a reference to call a virtual function
#include <iostream>
using namespace std;

class Box;  // Incomplete class definition required for prototype
void Output(Box& aBox); // Prototype of function

// Insert class definitions for Box and GlassBox
// (as in Ex8_08.cpp)

int main()
{
   Box myBox(2.0, 3.0, 4.0);          // Declare a base box
   GlassBox myGlassBox(2.0,3.0,4.0);  // Declare derived box

   Output(myBox);      // Output volume of base class object
   Output(myGlassBox); // Output volume of derived class object

   cout << endl;
   return 0;
}

// Function to output a volume via a virtual function
// call using a reference
void Output(Box& aBox)
{
   aBox.ShowVolume();
}

How It Works

At the beginning of the program, we have an incomplete definition of the class Box. This is included so that the compiler will know of the existence of Box as a class when it gets to the prototype of the function Output(). Without this, the prototype would cause an error message to be generated.

The function main() now basically consists of two calls of the function Output(), the first with an object of the base class as an argument and the second with an object of the derived class. Because the parameter is a reference to the base class, Output() accepts objects of either class as an argument and the appropriate version of the virtual function Volume() is called, depending on the object that is initializing the reference.

The program produces exactly the same output as the previous example, demonstrating that the virtual function mechanism does indeed work through a reference parameter.

Pure Virtual Functions

It's possible that you'd want to include a virtual function in a base class so that it may be redefined in a derived class to suit the objects of that class, but that there is no meaningful definition you could give for the function in the base class.

For example, we could conceivably have a class Container, which could be used as a base for defining our Box class, or a Bottle class, or even a Teapot class. The Container class wouldn't have data members, but you might want to provide a virtual member function Volume() for any derived classes. Since the Container class has no data members, and therefore no dimensions, there is no sensible Volume() definition that we can write. We can still define the class, however, including the member function Volume(), as follows:

// Listing 8_10-01
class Container   // Generic base class for specific containers
{
   public:
      // Function for calculating a volume - no content
      // This is defined as a 'pure' virtual function,
      // signified by '=0'
      virtual double Volume() = 0;

      // Function to display a volume
      virtual void ShowVolume()
      {
         cout << endl
              << "Volume is " << Volume();
      }
};

The statement for the virtual function Volume() defines it as having no content by placing the equals sign and zero in the function header. This is called a pure virtual function. Any class derived from this class must either define the Volume() function or redefine it as a pure virtual function.

The class also contains the function ShowVolume(), which will display the volume of objects of derived classes. Since this is declared as virtual, it can be replaced in a derived class, but if it isn't, the base class version that you see here will be called.

Abstract Classes

A class containing a pure virtual function is called an abstract class. It's called abstract because you can't define objects of a class containing a pure virtual function. It exists only for the purpose of a base class from which you can derive other classes. If a class derived from an abstract class still defines a pure virtual function of the base as pure, it too is an abstract class.

You should not conclude, from the example of the Container class above, that an abstract class can't have data members. An abstract class can have both data members and function members. The presence of a pure virtual function is the only condition that determines that a given class is abstract. In the same vein, an abstract class can have more than one pure virtual function. In this case, a derived class must have definitions for every pure virtual function in its base, otherwise it too will be an abstract class.

Try It Out - An Abstract Class

We could implement a Can class, representing beer or cola cans perhaps, together with our original Box class. Both are derived from the Container class. The definitions of these two classes would be as follows:

// Listing 8_10-02
class Box: public Container        // Derived class
{
   public:
      // Function to show the volume of an object
      virtual void ShowVolume()
      {
         cout << endl
              << "Box usable volume is " << Volume();
      }

      // Function to calculate the volume of a Box object
      virtual double Volume()
      { return length*breadth*height; }

      // Constructor
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
                       :length(lv), breadth(bv), height(hv){}

   protected:
      double length;
      double breadth;
      double height;
};

// Listing 8_10-03
class Can: public Container
{
   public:
      // Function to calculate the volume of a can
      virtual double Volume()
      { return 0.25*PI*diameter*diameter*height; }
      // Constructor
      Can(double hv=4.0, double dv=2.0):
                                   height(hv), diameter(dv){}

   protected:
      double height;
      double diameter;
};

The Box class is essentially as we had it in the previous example, except that this time we have specified that it is derived from the Container class. The Volume() function is fully defined within this class (as it must be if this class is to be used to define objects). The only other option would be to specify it as a pure virtual function, since it is pure in the base class, but then we couldn't create Box objects.

The Can class also defines a Volume() function based on the formula hπr2, where h is the height and r is the radius of the cross-section of a can. This is essentially the height multiplied by the area of the base to produce the volume. The expression in the function definition assumes a global constant PI is defined, so we'll need to remember that. You will also notice that we have redefined the ShowVolume() function in the Box class, but not in the Can class. We will see what effect this has when we get some program output.

We can exercise these classes with the following main() function:

// Ex8_10.cpp
// Using an abstract class
#include <iostream>             // For stream I/O
using namespace std;

const double PI= 3.14159265;    // Global definition for PI

// Insert definition of Container class (Listing 8_10-01)
// Insert definition of Box class       (Listing 8_10-02)
// Insert definition of Can class       (Listing 8_10-03)

int main()
{
   // Pointer to abstract base class
   // initialized with address of Box object
   Container* pC1 = new Box(2.0,3.0,4.0);

   // Pointer to abstract base class
   // initialized with address of Can object
   Container* pC2 = new Can(6.5, 3.0);

   pC1->ShowVolume();          // Output the volumes of the two
   pC2->ShowVolume();          // objects pointed to
   cout << endl;

   delete pC1;                 // Now clean up the free store
   delete pC2;                 // ....
   return 0;
}

How It Works

In this program, we declare two pointers to the base class, Container. Although we can't define Container objects (because Container is an abstract class), we can still define a pointer to a Container, which we can then use to store the address of a derived class object. The pointer pC1 is assigned the address of a Box object created in the free store by the operator new. The second pointer is assigned the address of a Can object in a similar manner.

Of course, because the derived class objects were created dynamically, we need to use the operator delete to clean up the free store when we have finished with them.

The output produced by this example is as follows:

Because we have defined ShowVolume() in the Box class, the derived class version is called for the Box object. We did not define this function in the Can class, so the base class version that the Can class inherits from Container is invoked for the Can object. Since Volume() is a virtual function that is implemented in both derived classes (necessarily, because it is a pure virtual function in the base class), the call to it is resolved when the program is executed by selecting the version belonging to the class of the object being pointed to. Thus, for the pointer pC1, the version from the class Box is called and, for the pointer pC2, the version in the class Can is called. In each case, therefore, we obtain the correct result.

We could equally well have used just one pointer and assigned the address of the Can object to it (after calling the Volume() function for the Box object). A base class pointer can contain the address of any derived class object, even when several different classes are derived from the same base class, and so we can have automatic selection of the appropriate virtual function across a whole range of derived classes. Impressive stuff, isn't it?

Indirect Base Classes

At the beginning of this chapter, we said that one class's base could in turn be derived from another, 'more' base class. A small extension of the last example will provide us with an illustration of this, as well as demonstrating the use of a virtual function across a second level of inheritance.

Try It Out - More than One Level of Inheritance

All we need to do is add the class GlassBox to the classes we have in the last example. The relationship between the classes we now have is illustrated below.

The class GlassBox is derived from the Box class exactly as before, but we will omit the derived class version of ShowVolume() to show that the base class version still propagates through the derived classes. With the class hierarchy shown above, the class Container is an indirect base of the class GlassBox and a direct base of the classes Box and Can.

Our new example, with an updated function main() to use the additional class in the hierarchy, will be as follows:

// Ex8_11.cpp
// Using an abstract class with multiple levels of inheritance
#include <iostream>               // For stream I/O
using namespace std;

const double PI= 3.14159265;       // Global definition for PI

class Container   // Generic base class for specific containers
{
   public:
      // Function for calculating a volume - no content
      // This is defined as a 'pure' virtual function,
      // signified by '=0'
      virtual double Volume() = 0;

      // Function to display a volume
      virtual void ShowVolume()
      {
         cout << endl
              << "Volume is " << Volume();
      }
};

class Box: public Container        // Derived class
{
   public:
      // Function to calculate the volume of a Box object
      virtual double Volume()
      { return length*breadth*height; }

      // Constructor
      Box(double lv=1.0, double bv=1.0, double hv=1.0)
                         :length(lv), breadth(bv), height(hv){}

   protected:
      double length;
      double breadth;
      double height;
};

class Can: public Container
{
   public:
      // Function to calculate the volume of a can
      virtual double Volume()
      { return 0.25*PI*diameter*diameter*height; }

      // Constructor
      Can(double hv=4.0, double dv=2.0):
                                   height(hv), diameter(dv){}

   protected:
      double height;
      double diameter;
};

class GlassBox: public Box     // Derived class
{
   public:
      // Function to calculate volume of a GlassBox
      // allowing 15% for packing
      virtual double Volume()
      { return 0.85*length*breadth*height; }

      // Constructor
      GlassBox(double lv, double bv, double hv): Box(lv,bv,hv){}
};

int main()
{
   // Pointer to abstract base class initialized
   // with Box object address
   Container* pC1 = new Box(2.0,3.0,4.0);
   Can myCan(6.5, 3.0);                // Define Can object
   GlassBox myGlassBox(2.0, 3.0, 4.0); // Define GlassBox object

   pC1->ShowVolume();        // Output the volume of Box
   delete pC1;               // Now clean up the free store

   pC1 = &myCan;             // Put myCan address in pointer
   pC1->ShowVolume();        // Output the volume of Can

   pC1 = &myGlassBox;        // Put myGlassBox address in pointer
   pC1->ShowVolume();        // Output the volume of GlassBox

   cout << endl;
   return 0;
}

How It Works

We have the three-level class hierarchy shown in the previous illustration, with Container as an abstract base class, because it contains the pure virtual function, Volume(). The function main() now calls the function ShowVolume() three times, using the same pointer to the base class, but with the pointer containing the address of an object of a different class each time. Since ShowVolume() is not defined in any of the derived classes we have here, the base class version is called in each instance. A separate branch from the base Container defines the derived class Can.

The example produces the following output

This shows that we execute the three different versions of the function Volume(), according to the type of object involved.

Note that we need to delete the Box object from the free store before we assign another address value to the pointer. If we don't do this, we wouldn't be able to clean up the free store, because we would have no record of the original address. This is an easy mistake to make when reassigning pointers and using the free store.

Virtual Destructors

One problem that arises when dealing with objects of derived classes by using a pointer to the base class is that the correct destructor may not be called. We can show this effect by modifying the last example.

Try It Out - Calling the Wrong Destructor

We just need to add destructors to each of the classes so that we can track which destructor is called when the objects are destroyed. Therefore, the program would be as follows:

// Ex8_12.cpp
// Destructor calls with derived classes
// using objects via a base class pointer
#include <iostream>          // For stream I/O
using namespace std;

const double PI= 3.14159265; // Global definition for PI

class Container              // Generic base class for containers
{
   public:

      // Destructor
      ~Container()
      { cout << "Container destructor called" << endl; }

      // Insert other members of Container as in Ex8_11.cpp
};

class Box: public Container        // Derived class
{
   public:

      // Destructor
      ~Box()
      { cout << "Box destructor called" << endl; }

      // Insert other members of Box as in Ex8_11.cpp
};

class Can: public Container
{
   public:

      // Destructor
      ~Can()
      { cout << "Can destructor called" << endl; }

      // Insert other members of Can as in Ex8_11.cpp
};

class GlassBox: public Box     // Derived class
{
   public:

      // Destructor
      ~GlassBox()
      { cout << "GlassBox destructor called" << endl; }

      // Insert other members of GlassBox as in Ex8_11.cpp
};

int main()
{
   // Pointer to abstract base class initialized
   // with Box object address
   Container* pC1 = new Box(2.0,3.0,4.0);
   Can myCan(6.5, 3.0);                // Define Can object
   GlassBox myGlassBox(2.0, 3.0, 4.0); // Define GlassBox object

   pC1->ShowVolume();                 // Output the volume of Box

   // Now clean up the free store
   cout << endl << "Delete Box" << endl;
   delete pC1;

   // Create GlassBox dynamically and output its volume...
   pC1 = new GlassBox(4.0, 5.0, 6.0);
   pC1->ShowVolume();
   // ...and delete it
   cout << endl << "Delete GlassBox" << endl;
   delete pC1;

   pC1 = &myCan;          // Get myCan address in pointer
   pC1->ShowVolume();     // Output the volume of Can
   pC1 = &myGlassBox;     // Get myGlassBox address in pointer
   pC1->ShowVolume();     // Output the volume of GlassBox

   cout << endl;
   return 0;
}

How It Works

Apart from adding a destructor to each class, which outputs a message to the effect that it was called, the only other change is a couple of additions to the function main(). There are additional statements to create a GlassBox object dynamically, output its volume and then delete it. There is also a message displayed to indicate when the dynamically created Box object is deleted. The output generated by this example is shown below:

You can see from this that, when we delete the Box object pointed to by pC1, the destructor for the base class Container is called. Similarly, when the GlassBox object that we added is deleted, again the destructor for the base class Container is called. For the other objects, the correct destructor calls occur with the derived class constructor being called first, followed by the base class constructor. For the first GlassBox object created in a declaration, three destructors are called: first, the destructor for the derived class, followed by the direct base destructor and, finally, the indirect base destructor.

All the problems are with objects created in the free store. In both cases, the wrong destructor is called. The reason for this is that the linkage to the destructors is resolved statically, at compile time. For the automatic objects, there is no problem - the compiler knows what they are and arranges for the correct destructors to be called.

With objects created dynamically and accessed through a pointer, things are different. The only information that the compiler has when the delete operation is executed is that the pointer type is a pointer to the base class. The type of object the pointer is actually pointing to is unknown. The compiler then simply ensures that the delete operation is set up to call the base class destructor. In a real application, this can cause a lot of problems, with bits of objects left strewn around the free store and possibly more serious problems, depending on the nature of the objects involved.

The solution is simple. We need the calls to be resolved dynamically as the program is executed. We can organize this by using virtual destructors in our classes. As we said when we first discussed virtual functions, it's sufficient to declare the base class function as virtual to ensure that all functions in any derived classes with the same name, parameter list and return type are virtual as well. This applies to destructors just as it does to ordinary member functions. We need to add the keyword virtual to the definition of the destructor in the class Container, so that it becomes as follows:

class Container        // Generic base class for containers
{
   public:
      // Destructor
      virtual ~Container()
      { cout << "Container destructor called" << endl; }

      // Insert other members of Container as in Listing 8_10-01
};

Now the destructors in all the derived classes are automatically virtual, even though you don't explicitly specify them as such. Of course, you're free to specify them as virtual, if you want the code to be absolutely clear.

If you rerun the example with this modification, it will produce the following output:

As you can see, all the objects are now destroyed with a proper sequence of destructor calls. Destroying the dynamic objects produces the same sequence of destructor calls as the automatic objects of the same type in the program.

It's a good idea always to declare your base class destructor as virtual as a matter of course when using inheritance. There is a small overhead in the execution of the class destructors, but you won't notice it in the majority of circumstances. Using virtual destructors ensures that your objects will be properly destroyed and avoids potential program crashes that might otherwise occur.


(Multiple Inheritance)

Multiple Inheritance

All our derived classes so far have had a single direct base class, but we aren't limited to that. In fact, a derived class can have several base classes; this is referred to as multiple inheritance. This means, of course, that multiple indirect bases are also possible.

Multiple Base Classes

It's quite difficult to come up with an example of a class with multiple base classes that is based on relationships in the real world. Defining a class, such as Box, in terms of the class Container reflects the real-world relationship between a box and a container. A box is a form of container, so we are defining a more specific object from a more general one. With most real-world objects, this unidirectional specialization pattern applies. Multiple base classes are often used in practice for the convenience of implementation rather than to reflect any particular relationships between objects.

However, we could consider the example of a Package, which might be a combination of a Container, or some specialized form of container such as a Box, together with the contents of the container defined by a class Contents. We could define the class Package as derived from both the class Box and the class Contents. This could be represented like so:

The definition of the class would look like this:

class Package: public Box, public Contents
{
...
};

The class Package will now inherit all the members of both classes, with the same access specifiers as appear in the definitions of the base classes, since they are defined as public base classes. There is no access to members of the base class declared as private, however; the access limitations for inherited class members, which we discussed earlier in this chapter, apply equally well to classes with multiple bases.

Things can get a little more complicated now. For example, it's conceivable that both base classes could have a public member function, Show(), to display the contents of an object. We could have a statement to call the Show() member function of an object of the Package class, myPackage, like this:

myPackage.Show();

This statement, however, is ambiguous, since the class Package contains two members with the same name, Show(), one inherited from each of the base classes. The compiler has no way of knowing which one should be called, so this will result in an error message. If you need to call one or the other, you have to use the scope resolution operator to specify which of the two functions you want to invoke. For example, you could write this,

myPackage.Contents::Show();

which makes it quite clear that you want to call the function that is inherited from the class Contents.

Virtual Base Classes

A further complication can arise with multiple inheritance if the direct base classes are themselves derived from another class or classes. The possibility arises that both base classes could be derived from a common class. For instance, the classes Contents and Box, which we used in the definition of the class package, could be derived from another base called RockBottom. Their definitions could then be something like this:

class Contents: public RockBottom
{
...
};

class Box: public RockBottom
{
...
};

Now the class Package will contain two copies of the members of the class RockBottom, as illustrated here:

In the class Package, we end up with two data members called x, and two function members Show(). The duplication of the members of the indirect base can at best be confusing, and at worst it can cause a lot of problems. However, it's easy to avoid by simply modifying the definitions of the base classes Box and Contents such that the class RockBottom is specified as a virtual base class. Their definitions in outline would then be as follows:

class Contents: public virtual RockBottom
{
...
};

class Box: public virtual RockBottom
{
...
};

Now there will be only one instance of the members of the base class RockBottom in any class derived from these two classes, and any problems with the original definition of the class Package disappear. Specifying the base class as virtual instructs the compiler to make sure that the data members are not duplicated in a derived class, even where there are multiple occurrences of the class as an indirect base.


(Dynamic Casts)

Dynamic Casts

A dynamic cast is a cast that is performed at runtime. To specify a dynamic cast, you use the dynamic_cast<>() operator. You can only apply the dynamic_cast<>() operator to pointers and references to polymorphic class types - that is class types that contain at least one virtual function. The reason is that only pointers to polymorphic class types contain the information that the dynamic_cast<>() operator needs to check the validity of the cast. The dynamic_cast<>() operator is specifically for converting between pointers or between references to class types in a hierarchy. Note that the types you are casting between must be pointers or references to classes within the same class hierarchy (though not necessarily in a single leg of a hierarchy). You can't use dynamic_cast<>() for anything else. We will look at dynamically casting pointers first.

Dynamically Casting Pointers

There are two basic types of dynamic cast that we can distinguish. The first is a cast down a hierarchy, from a pointer to a direct or indirect base type to a pointer to a derived type. This is referred to as a downcast. The second is a cast across a hierarchy. This is referred to as a crosscast. Examples of both of these possibilities are illustrated below.

For a pointer pBox, of type 'pointer to Box', we could write the downcast shown in the diagram as:

Carton* pCarton = dynamic_cast<Carton*>(pBox);

As you see, the dynamic_cast<>() operator is written in the same way as the static_cast<>() operator. The destination type goes between the angled brackets following dynamic_cast<>(), and the expression that you want to be cast to the new type goes between the parentheses. For this cast to be legal, the classes Box and Carton must contain virtual functions, either as declared or inherited members. For the cast to work, pBox must point to either a Carton object or a CerealPack object, since only objects of these types contain a Carton sub-object. If the cast does not succeed, the pointer pCarton will be set to 0.

The crosscast shown above could be written:

Contents* pContents = dynamic_cast<Contents*>(pBox);

As in the previous case, here both the Contents and the Box classes must both be polymorphic for the cast to be legal. The cast can only succeed if pBox contains the address of an object of type CerealPack, because this is the only type that contains a Contents object and can be referred to using a pointer of type Box*. If it does not succeed, null will be stored in pContents.

Using dynamic_cast<>() to cast down a class hierarchy may fail, but in contrast to the static cast the result will be a null pointer, not an undefined result. This provides a clue as to how you can use this. Suppose you have some kind of object pointed to by a pointer to Box, and you wanted to call a non-virtual function member of the Carton class. You can only call virtual functions of a derived class through a base class pointer, but the dynamic_cast<>() operator can enable you to call a non-virtual function. Suppose surface() is a non-virtual function member of the Carton class. You could call it with the statement:

dynamic_cast<Carton*>(pBox)->surface();

This is obviously hazardous. You still need to be sure that pBox points to a Carton object or an object of a class that has the Carton class as a base. If it doesn't, the dynamic_cast<>() operator will return null and the call will fail. However, you can use the dynamic_cast<>() operator to determine whether or not what you intend to do is valid. For example:

if(Carton* pCarton = dynamic_cast<Carton*>(pVessel))
  pCarton->surface();

Now we will only attempt the function call if the result of the cast is not null.

Note that you cannot remove constness with dynamic_cast<>(). If the pointer type you are casting from is const, then the pointer type you are casting to must also be const. If you want to cast from a const pointer to a non-const pointer, you must cast to a non-const pointer of the same type as the original by using the const_cast<>() operator.


(Summary)

Summary

In this chapter, we've covered all of the principal ideas involved in using inheritance. The fundamentals that you should keep in mind are these:

  • A derived class inherits all the members of a base class except for constructors, the destructor and the overloaded assignment operator.

  • Members of a base class which are declared as private in the base class are not accessible in any derived class. To obtain the effect of the keyword private but allow access in a derived class, you should use the keyword protected in place of private.

  • A base class can be specified for a derived class with the keyword public, private, or protected. If none is specified, the default is private. Depending on the keyword specified for a base, the access level of the inherited members may be modified.

  • If you write a derived class constructor, you must arrange for data members of the base class to be initialized properly, as well as those of the derived class.

  • A function in a base class may be declared as virtual. This allows other definitions of the function appearing in derived classes to be selected at execution time, depending on the type of object for which the function call is made.

  • You should declare the destructor in a base class containing a virtual function as virtual. This will ensure correct selection of a destructor for dynamically created derived class objects.

  • A class may be designated as a friend of another class. In this case, all the function members of the friend class may access all the members of the other class. If class A is a friend of B, class B is not a friend of A unless it has been declared as such.

  • A virtual function in a base class can be specified as pure by placing =0 at the end of the function declaration. The class will then be an abstract class for which no objects can be created. In any derived class, all the pure virtual functions must be defined; if not, it too becomes an abstract class.

  • A class may be derived from multiple base classes, in which case it inherits members from all of its bases, with the exception of destructors, constructors and overloaded assignment operator functions.

  • An indirect base class should be specified as virtual for derived classes when using multiple inheritance, in order to avoid duplicate occurrences of its data members in a class with multiple bases, two or more of which are derived from the indirect base.

We have now gone through all of the important language features of C++, and by now you should be comfortable with most of them. You can go back and play with the source code to try out your new skills - you could try to extend the existing examples we've been through. Remember that the code for all of the examples is available for download form the Wrox Press website: http://www.wrox.com.


Welcome to the Wrox Press C++ tutorial

"I hope you'll enjoy reading this tutorial with your portable, your work, or your home PC. It's a perfect companion to the Introduction to Visual C++ 6.0 Standard Edition manual and is a proven aid to understanding the C++ language. The material in this tutorial is adapted from my book Beginning Visual C++ 6.0, to provide you with a thorough grounding in 'pure' C++. I've been careful to address the new standards in C++ laid out by the ANSI and ISO committees and I encourage you to adopt these conventions so that your programs are maintainable for years to come.

There's plenty of code in this tutorial, which you can cut and paste from the help viewer and is also available for download from the Wrox Press web site. We've tested this code extensively, but if you do encounter a problem, try visiting the web site, which will list any errata that may have been spotted since publication of this document. Also, remember that your suggestions for improving the text are very welcome - use the web site to give us your feedback. We try to keep all Wrox tutorials error free and appreciate your help in sustaining our high standards."

Ivor Horton

©1998 Wrox Press Ltd
http://www.wrox.com/